-
Notifications
You must be signed in to change notification settings - Fork 47k
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
useMemo / useCallback cache busting opt out #15278
Comments
If you want to rely on it as a semantic guarantee, using a ref sounds like the way to go. Why is that not working out for you? |
Using a ref is working for me, but I thought it would be nice to not need to use an alternative. I suspect other people might also want to opt out of cache purging if I do. Also, moving away from useMemo and useCallback currently means losing some of the value provided by |
Even if |
@alexreardon How are you computing the value before useEffect runs? Or are you doing the should-it-update checks manually? |
My current approach is fairly naive and needs to be thought through: It currently does not use |
Ah ya the manual approach. Probably uses more memory than if it were baked into React. Definitely adds more bundle bloat. Thanks @alexreardon ! |
I think this is an uncommon enough case that using
I don't think it's valuable to document the internal caching strategy beyond "React might clear the cache when it needs to" since it's likely to be dynamic and difficult to predict. No single component could predict cache hits or misses at runtime with any certainty.
Just a heads up that this is likely to be problematic in concurrent mode, since the function might be called many times with different props. |
What is the recommendation then for this behaviour? useEffect? When does |
@aweary Interesting, so you don't think caching the promise thrown for Suspense will be common? I hear if you return a different promise everything breaks. |
Here is an alternative // @flow
import { useRef, useState, useEffect } from 'react';
import areInputsEqual from './are-inputs-equal';
type Result<T> = {|
inputs: mixed[],
result: T,
|};
export default function useMemoOne<T>(
// getResult changes on every call,
getResult: () => T,
// the inputs array changes on every call
inputs?: mixed[] = [],
): T {
// using useState to generate initial value as it is lazy
const initial: Result<T> = useState(() => ({
inputs,
result: getResult(),
}))[0];
const uncommitted = useRef<Result<T>>(initial);
const committed = useRef<Result<T>>(initial);
// persist any uncommitted changes
useEffect(() => {
committed.current = uncommitted.current;
});
if (areInputsEqual(inputs, committed.current.inputs)) {
return committed.current.result;
}
uncommitted.current = {
inputs,
result: getResult(),
};
return uncommitted.current.result;
} |
If there are no deps I think that |
I have shipped |
We also have cases where const {keyword} = props;
const keywordList = useMemo(
() => keyword.split(' '),
[keyword]
);
const flipList = useCallback(
() => {
// something about keywordList
},
[keywordList]
);
useEffect(
() => {
someSideEffectWithKeywordList(keywordList);
},
[keywordList]
);
Currently we try to get rid of this risk by computing |
@otakustay can you rely on |
I tried this, then I encountered 2 issues:
|
@alexreardon Docs only say that:
If |
@bhovhannes useMemo(() => e => e.stopPropagation()) isn't as pleasant to read or write as useCallback(e => e.stopPropagation()) and the double |
@urugator Yup const stash = useRef({}).current
if (!stash.foo) stash.foo = ...
stash.onChange = props.onChange
...
useEffect(() => {
const {onChange} = stash
onChange(value)
}, [value]) |
@alexreardon @urugator I haven't read much into concurrent mode but would the following work? function useMemoOne(compute, deps) {
const stash = useRef({}).current
if (!stash.initialized) stash.value = compute()
useEffect(() => {
if (!stash.initialized) stash.initialized = true
else stash.value = compute()
}, deps)
return stash.value
}
function useCallbackOne(callback, deps) {
const ref = useRef(callback)
useEffect(() => {
ref.current = callback
}, deps)
return ref.current
} |
@jedwards1211 No, const useMemoOne = (compute, deps) => {
const value = useRef(compute);
const previousDeps = useRef(deps);
if (!shallowEquals(previousDeps, deps)) {
previousDeps.current = deps;
value.current = compute();
}
return value.current;
}; |
I was about to delete my comment, I wasn't thinking about how it would be too late. I don't really understand why this side effect during render would be more problematic for concurrent mode than anything else, as @aweary implied. Will |
|
Concurrent mode is not an issue. Problem is that effect runs after render, meaning that when deps are changed, there is one render pass during which memoized value is out of sync with the rest of the props. |
Yeah I realized the useEffect render sync issue. I was asking why a hard side effect like in @otakustay's example, instead of useEffect, would be any more of a problem for concurrent mode. Or put another way, if all the complexity in @alexreardon's useMemoOne is necessary, or if @otakustay's implementation would suffice. |
I don't think so. The only problem with @otakustay solution is that |
I can't edit the issue, but I think the title is meant to be "cache purging" not "cache busting". |
@alexreardon What is the difference between the package const useMemoOne = (compute, deps, equalityFn = shallowEqual) => {
const value = useRef(compute);
const previousDeps = useRef(deps);
if (!equalityFn(previousDeps, deps)) {
previousDeps.current = deps;
value.current = compute();
}
return value.current;
};
const useCallbackOne = (compute, deps, equalityFn) => useMemoOne(() => compute, deps, equalityFn); |
@bertho-zero That means Now, the problem is that Also, I wouldn't change the semantics of hooks inputs, so Also, there's another bug: You're comparing Try this: function useMemoOne(compute, deps) {
const isNew = useRef(true);
const value = useRef(isNew.current ? compute() : null);
const previousDeps = useRef(deps);
isNew.current = false;
if (!(
Array.isArray(deps)
&& Array.isArray(previousDeps.current)
&& deps.length === previousDeps.current.length
&& deps.every((dep, index) => dep === previousDeps.current[index]
)) {
previousDeps.current = deps;
value.current = compute();
}
return value.current;
} |
@steve-taylor
Additionally the deps comparison can be avoided on the initial render like @urugator mentioned:
The only issue left is that when |
@fabb there’s nothing magical about The change you made causes Edit: Your second version looks like it might work, because now you’re not evaluating |
@steve-taylor you are completely right, thanks. I've written some unit tests to see what really happens:
As expected the second test fails (same with the version that uses Here is a codesandbox with the failing unit test: https://codesandbox.io/s/usememoone-test-entsz?fontsize=14&hidenavigation=1&module=%2Fsrc%2F__tests__%2FuseMemoOne.test.tsx&theme=dark |
I have a different question: from this article and the vague documentation, I had the impression that React might keep not only the latest, but also previous values and deps arrays too, which could become a memory problem:
Source: https://kentcdodds.com/blog/usememo-and-usecallback But unit tests could not verify that (see codesandbox). Now my question is, can we take it for granted that React only keeps the latest value (cc @gaearon)? React keeping several previous values and deps arrays could cause quite some unwanted memory increase. It might make sense to improve the React documentation on these points. |
So is the above genuinely the simplest way to implement useMemo with a semantic guarantee safely?... surely all that complexity warrants inclusion in React itself or at least something in the React docs pointing to Unless maybe I'm doing something wrong, however I find I need to perform change detection on derived data quite often 🤔. I wonder if users are generally getting away with |
@aweary but does it also mean we cannot rely on ps sorry for late call. |
Functions returned from Here is a link to the same concern as I described above. |
We also use function returned from useCallback as dependency to useEffect for data fetching. Memoized function often comes from another custom hook. Cache busting will cause state to be overwriten. |
Not sure if it's been mentioned, but |
According to the
React
docs,useMemo
anduseCallback
are subject to cache purging:I am working on moving
react-beautiful-dnd
over to using hooks atlassian/react-beautiful-dnd#871. I have the whole thing working and tested 👍It leans quite heavily on
useMemo
anduseCallback
right now. If the memoization cache for is cleared for a dragging item, the result will be a cancelled drag. This is not good.My understanding is that
useMemo
anduseCallback
are currently not subject to cache purging based on this language:Request 1: Is it possible to opt-out of this cache purging? Perhaps a third
options
argument touseMemo
anduseCallback
:(Naming up for grabs, but this is just the big idea)
A work around is to use a custom memoization toolset such as a
useMemoOne
which reimplementsuseMemo
anduseCallback
just usingref
s see exampleI am keen to avoid the work around if possible.
Request 2: While request 1 is favourable, it would be good to know the exact conditions in which the memoization caches are purged
The text was updated successfully, but these errors were encountered: