-
Notifications
You must be signed in to change notification settings - Fork 91
Optimize condition and field array performance #1017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1017 +/- ##
==========================================
+ Coverage 84.80% 85.06% +0.26%
==========================================
Files 199 200 +1
Lines 3264 3321 +57
Branches 1115 1130 +15
==========================================
+ Hits 2768 2825 +57
Misses 496 496
Continue to review full report at Codecov.
|
I have thought of one possible issue and that is we are not accepting the dot notation when constructing the reduced values object. Ill adjust the code. |
As we discussed in DMs, there is the issue with an unwanted registering fields that are being created in the condition wrapper. We should make this setting opt-in and describe the caveat in the documentation, or we could update the getRegisteredFields to ignore fields created by conditions (however, the question is then how to know what fields are created by conditions or by the useFieldHook) Or we can wait until our state manager is implemented - then it would be simple to implement FieldSpy. 😈 edit: Theoretically we can make an array in a ref (so no rerenders) and update it in the useFieldApi hooks - and then return the array in the method instead of the FF's one. 🤔 However, not sure if there could be any side-effects of this solution. |
schema for checking the wizard const wizardSchema = {
fields: [
{
component: componentTypes.WIZARD,
name: 'wizzard',
buttonLabels: {},
title: 'Cokoliv',
fields: [
{ name: 'first', nextStep: ({values: {path}}) => path ? 'path1' : 'path2', fields: [{
name: 'path',
component: componentTypes.SWITCH,
label: 'Path 1?'
}]},
{
name: 'path1',
nextStep: 'summary',
fields: [{
component: componentTypes.TEXT_FIELD,
name: 'text1',
label: 'text1',
validate: [{type: 'required'}]
}]
},
{
name: 'path2',
nextStep: 'summary',
fields: [{
component: componentTypes.TEXT_FIELD,
name: 'text2',
label: 'text2',
validate: [{type: 'required'}]
}]
},
{
name: 'summary',
fields: [{
name: 'summary',
component: componentTypes.TEXTAREA,
label: 'summary',
condition: {
or: [
{when: 'text1', isNotEmpty: true},
{when: 'text2', isNotEmpty: true},
]
}
}]
}
]
}
]
}; |
@rvsia I've added a custom field registration method. The wizard works with the new registration. There I've kept the FF getRegisteredFields under an alias. That part is ready. I will check the field array and what might be the bottleneck here. |
@@ -26,9 +26,20 @@ const FormRenderer = ({ | |||
...props | |||
}) => { | |||
const [fileInputs, setFileInputs] = useState([]); | |||
const [registeredFields, setRegisteredFields] = useState({}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about to use ref to not trigger any renders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this triggers any additional renders of the children 🤔 Let me check the profiler. But, using ref might cause some race conditions because we can't use a callback to override the value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, it does 🤦 we don't shield the render from parent re-renders. I'm just gonna slap the ref there and we will see what it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to use a callback. It's an array, so you can modify it and the reference is still the same. And you don't have to use an object: use array with duplicates and the function will return a Set from the array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK it almost totally undid all the work. We can mimic the callback from setState which will make using ref safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So register just pushes the name into the array, unregister removes the first occurrence of the name and getRegistered returns the array without duplicates.
ee0d6a9
to
044f6de
Compare
|
||
const name = internalTriggers.shift(); | ||
return ( | ||
<FieldProvider skipRegistration name={name} subscription={{ value: true }} render={({input: {value}}) => ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still use the FF field - we don't need any of our useFieldApi features (and so we don't have to use any skipRegistration
)
@@ -111,6 +159,8 @@ SingleField.propTypes = { | |||
resolveProps: PropTypes.func | |||
}; | |||
|
|||
const renderForm = (fields) => fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />)); | |||
const renderForm = (fields) => { | |||
return fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return block
044f6de
to
d5a7ed8
Compare
d5a7ed8
to
4fa3cd5
Compare
@rvsia I think it's ready. Feel free to dive into the code. |
@@ -69,7 +69,8 @@ | |||
"react-dom": ">=16.13.0" | |||
}, | |||
"dependencies": { | |||
"@data-driven-forms/common": "*" | |||
"@data-driven-forms/common": "*", | |||
"lodash": "^4.17.21" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't lodash dependency of form-renderer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but now lodash is a direct dependency of the package and should be listed.
const internalRegisterField = (name) => { | ||
setRegisteredFields(prev => prev[name] ? ({...prev, [name]: prev[name] + 1}) : ({...prev, [name]: 1})); | ||
}; | ||
|
||
const internalUnRegisterField = (name) => { | ||
setRegisteredFields(({[name]: currentField, ...prev}) => currentField && currentField > 1 ? ({[name]: currentField - 1, ...prev}) : prev); | ||
}; | ||
|
||
const internalGetRegisteredFields = () => Object.entries(registeredFields.current).reduce((acc, [name, value]) => value > 0 ? [...acc, name] : acc, []); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about to do what I proposed (this seems unnecessary complex to me):
const registeredFields = useRef([]);
register:
const internalRegisterField = (name) => registeredFields.current.push(name)
unregister:
function removeElement(array, elem) {
var index = array.indexOf(elem);
if (index > -1) {
array.splice(index, 1);
}
}
const internalUnRegisterField = (name) => removeElement(registeredFields.current, name)
internalGetRegisteredFields
const internalGetRegisteredFields = () => new Set(registeredFields.current)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a couple of potential issues. First, I don't like the idea of having the same item multiple times inside the array. Using an object is slightly more resource-efficient than looking for indexes.
Another problem might be the usage of push
which can create an out-of-sync structure when we re-create the array (splice in removeElement). Theoretically, we can push into an array that no longer exists.
Also this:
const internalGetRegisteredFields = () => new Set(registeredFields.current)
Would return an instance of a set instead of an array. We would need either Array.from
or [...x]
destructor to actually get an array to preserve current API. So we need some value transformation anyway.
<ConditionTriggerDetector triggers={triggers} condition={condition} field={field}> | ||
{children} | ||
</ConditionTriggerDetector> | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strange formatting here
🎉 This PR is included in version 3.3.2 🎉 The release is available on Demo can be found here! |
fixes #1016
Description
The issue was that any field wrapped in the condition is subject to render, even if the
when
value did not change. This introduced improvedFormSpy
that listens only on a set of values in the form state. The effect of the new spy is that we no longer trigger render cycle in every condition wrapped field and by that extension skips the condition evaluation because it's not required.See profiler snapshots and examples in the linked issue.