Not allowing dependency lists to change size limits the usefulness of useMemo in this particular use case but I imagine there are other similar use cases.
To be clear I am talking about the error triggered here:
For instance, in my app I have a bunch of items and the user can select an unlimited amount of them, in another component I want to compute an expensive derived value based on this selection that is relevant only to this component, a good use case for useMemo.
However it is not currently possible to use useMemo and I am forced to compute this derived data outside of this component even though I am only interested in doing so whilst this component is mounted.
I don't understand why a change in dependency list length cannot be assumed to be a change in the dependencies itself?
I believe this can be implemented by changing the above to:
if (prevDeps.length !== nextDeps.length) {
return false;
}
This is seems like a non-problem: just add an array itself as a single dependency.
@vkurchatkin I can't guarantee the referential equality of the array (it is derived itself, a list of ids mapped against a dictionary). The items within it stay referentially the same unless explcitily modified.
Trying to keep the array referentially equal creates a whole host of cache invalidation issues and becomes very fragile in its implementation.
This is a problem anywhere where useMemo would want to reduce a variable length list.
Generally in React you should:
If you follow these two rules, then useMemo just works.
is derived itself, a list of ids mapped against a dictionary
If it is derived, then you should derive it using useMemo as well
@vkurchatkin I am following all these rules.
I can't use useMemo to create it because of the issue above.
Consider this:
I have normalized dictionary of objects, they are immutable and I change it by doing shallow clones down to the appropriate level of change.
Elsewhere I store a list of selected IDs that reference the keys of that dictionary.
I map that list into an array of the selected objects. I do not want to useMemo that because the IDs don't change but the items do, therefore due to the immutable nature in which I update the items I would be left with stale copies in that array. To useMemo that effectively I need to pass the variable length list of items in as dependencies again.
I now want to derive some information from those selected items. I need useMemo to have the objects as dependencies. I am unable to because the size of the list of objects changes (the selection) and at the same the objects themselves will update (immutably hence why I can't store a referentially equal subset of them in an array).
In short I am trying to follow the principles but in this case useMemo does not work as I am unable to give it a variable dependency list when the data I am computing changes in size.
I could useMemo and pass in the dictionary as a whole buts its hugely inefficient and causes tons of unnecessary recalculations of that data, that I only need to recompute if the selected items change.
I don't understand the reasons behind this technical limitation, it feels weird to me that perhaps the simplest, cleanest, possible use of useMemo isn't supported out of the box:
useMemo ( () => x, x );
I'd like to see this, too.
As a slightly-contrived example, let's consider the following case:
function formValuesReducer(oldValues, update) {
return { ...oldValues, [update[0]]: update[1] };
}
const initialFormValues = {
firstName: 'John',
lastName: 'Doe',
city: 'Boise',
state: 'Idaho'
};
const nameTemplatePieces = ['firstName', 'lastName'];
const MyComponent = (props) => {
const formValues = useReducer(formValuesReducer, initialFormValues);
const nameValues = useMemo(
() => nameTemplatePieces.map(templatePieces => formValues[templatePieces]),
[formValues, nameTemplatePieces]
);
const fullName = useMemo(
() => doExpensiveOperation(nameValues),
[nameValues]
);
}
In the above example, fullName is recalculated any time that _any_ field in formValues changes. Ideally, though, fullName would only be recalculated when either nameValues.firstName or nameValues.lastName has changed.
Since nameTemplatePieces is a constant with a fixed length, we could guarantee that fullName only gets recalculated when nameValues.firstName or nameValues.lastName change by changing this:
const fullName = useMemo(
() => doExpensiveOperation(nameValues),
[nameValues]
);
to this:
const fullName = useMemo(
() => doExpensiveOperation(nameValues),
[...nameValues]
);
However, if the length of nameTemplatePieces (and therefore nameValues) isn't fixed, then this no longer works, because React throws an error saying that the same number of arguments must always be passed to useMemo()
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Yes I still care about this.
I agree with OP, I also don't understand why instead of throwing an error
prevDeps.length !== nextDeps.length can't just count as a change in the dependencies itself.
I want to be able to memoize an array but spreading over the values inside the dependency would throw an error if the array changes in size.
A temporary solution could be to use JSON.stringify(arr) inside hook's deps, but this will only work for serializable data.
Most helpful comment
I agree with OP, I also don't understand why instead of throwing an error
prevDeps.length !== nextDeps.lengthcan't just count as a change in the dependencies itself.I want to be able to memoize an array but spreading over the values inside the dependency would throw an error if the array changes in size.
A temporary solution could be to use
JSON.stringify(arr)inside hook's deps, but this will only work for serializable data.