-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
useCallback/useEffect support custom comparator #14476
Comments
You could do this instead. useCallback(() => {
doSth(a, b)
}, [a, a.prop, a.anotherProp, a.prop.nestedProp /* and so on */, b]) Or you could try using |
You can use the result of the custom equality check as a value inside the array to get this behavior. useEffect(
() => {
// ...
},
[compare(a, b)]
); |
@aweary How would that work? They want to use the custom comparator to check |
@heyimalex you can use a |
@aweary How would passing I was trying to write what the op wanted and here's what I got. function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function useCallbackCustomEquality(cb, args, equalityFunc) {
const prevArgs = usePrevious(args);
const argsAreEqual =
prevArgs !== undefined &&
args.length === prevArgs.length &&
args.every((v, i) => equalityFunc(v, prevArgs[i]));
const callbackRef = useRef();
useEffect(() => {
if (!argsAreEqual) {
callbackRef.current = cb;
}
})
return argsAreEqual ? callbackRef.current : cb;
} |
@kpaxqin yeah it's sort of a half-baked solution 😕your approach is fine I think for |
You could actually encapsulate the arg memoization in one hook and then use that for all of the other functions. function useMemoizeArgs(args, equalityCheck) {
const ref = useRef();
const prevArgs = ref.current;
const argsAreEqual =
prevArgs !== undefined &&
args.length === prevArgs.length &&
args.every((v, i) => equalityFunc(v, prevArgs[i]));
useEffect(() => {
if (!argsAreEqual) {
ref.current = args;
}
});
return argsAreEqual ? prevArgs : args;
}
useEffect(() => {
// ...
}, useMemoizeArgs([a,b], equalityCheck)); |
A similar API to react/packages/react-reconciler/src/ReactFiberHooks.js Lines 524 to 549 in 659c139
|
@heyimalex Thanks for your solution, it works great for me, but still got one risk as a common solution. For example: re-render every second, use current time as input array, compare time and run effect if minute has changed. The key is: Use another value as compare result could get rid of this function useMemoizeArgs(args, equalityCheck) {
const ref = useRef();
const flag = ref.current ? ref.current.flag : 0;
const prevArgs = ref.current ? ref.current.args : undefined;
const argsAreEqual =
prevArgs !== undefined &&
args.length === prevArgs.length &&
args.every((v, i) => equalityFunc(v, prevArgs[i]));
useEffect(() => {
ref.current = {
args,
flag: !argsAreEqual ? (flag + 1) : flag
}
});
return ref.current.flag
}
useEffect(() => {
// ...
}, useMemoizeArgs([a,b], equalityCheck)); Still think the best way would be support custom comparator officially for those hooks which need to compare inputs. |
The solution given by @kpaxqin didn't work during my tests because:
I modify it to: const useMemoizeArgs = (args, equalityFunc) => {
const ref = useRef();
const flag = ref.current ? ref.current.flag : 0;
const prevArgs = ref.current ? ref.current.args : undefined;
const argsHaveChanged =
prevArgs !== undefined &&
(args.length !== prevArgs.length || args.some((v, i) => equalityFunc(v, prevArgs[i])));
useEffect(() => {
ref.current = {
args,
flag: argsHaveChanged ? flag + 1 : flag,
};
});
return flag;
}; which works for my use case. But obviously if hooks supported a custom comparator like |
My main use cases where deep equal required is data fetching const useFetch = (config) => {
const [data, setData] = useState(null);
useEffect(() => {
axios(config).then(response => setState(response.data)
}, [config]); // <-- will fetch on each render
return [data];
}
// in render
const DocumentList = ({ archived }) => {
const data = useFetch({url: '/documents/', method: "GET", params: { archived }, timeout: 1000 })
...
} I checked top google results for "react use fetch hook", they are either have a bug (refetch on each rerender) or use one of the methods:
So it would be good to have a comparator as a third argument or a section in docs that explains the right way for such use cases. |
Right now we have no plans to do this. Custom comparisons can get very costly when spread across components — especially when people attempt to put deep equality checks there. Usually there's a different way to structure the code so that you don't need it. There are a few common solutions: Option 1: Hoist it upIf some value is supposed to be always static, there's no need for it to be in the render function at all. const useFetch = createFetch({ /* static config */});
function MyComponent() {
const foo = useFetch();
} That completely solves the problem. If instantiating it is annoying, you can always have your own // ./myUseFetch.js
import createFetch from 'some-library';
export const useFetch = createFetch({ /* config 1 */ })
export const useFetchWithAuth = createFetch({ /* config 2 */ }) Option 2: Embrace dynamic valuesAnother option is the opposite. Embrace that values are always dynamic, and ask the API consumer to memoize them. function MyComponent() {
const config = useMemo(() => ({
foo: 42,
}, []); // Memoized
const data = useFetch(url, config);
} This might seem like it's convoluted. However for low-level Hooks you probably will create higher-level wrappers anyway. So those can be responsible for correct memoization. // ./myUseFetch.js
import {useContext} from 'react';
import useFetch from 'some-library';
import AuthContext from './auth-context';
export function useFetchWithAuth(url) {
const authToken = useContext(AuthContext);
const config = useMemo(() => ({
foo: 42,
auth: authToken
}), [authToken]); // Only changes the config if authToken changes
return useFetch(url, config);
}
// ./MyComponent.js
function MyComponent() {
const data = useFethWithAuth('someurl.com'); // No config
} Then effectively the users don't need to configure anything at the call site — or you can expose limited supported options as specific arguments. That's probably a better API than an arbitrarily deep "options" object anyway. Option 3: (be careful) JSON.stringifyMany people expressed a desire for deep equality checks. However, deep equality checks are very bad because they have unpredictable performance. They can be fast one day, and then you change the data structure slightly, and they become super slow because the traversal complexity has changed. So we want to explicitly discourage using deep checks — and However, there are cases where it stands in the way of ergonomics too much. Such as with code like const variables = {userId: id};
const data = useQuery(variables); In this case there is no actual deep comparisons necessary. In this example, we know our data structure is relatively shallow, doesn't have cycles, and is easily serializable (e.g. because we plan to send that data structure over the network as a request). It doesn't have functions or weird objects like Dates. In those rare cases it's acceptable to pass “Wait!” I hear you saying, “Isn’t On small inputs, like the example above, it is very fast. It does get slow on larger inputs. But at that point deep equality is also slow enough that you’ll want to rethink your strategy and API. Such as going back to a granular invalidation with There are probably minor other use cases that don't quite fit into these. For those use cases it is acceptable to use refs as an escape hatch. But you should strongly consider to avoid comparing the values manually, and rethinking your API if you feel you need that. |
Dan listed useMemo as a solution for avoiding re-triggering an effect facebook/react#14476 (comment) If this is an accepted use case, the docs should reflect it.
Just commenting that this is an issue with ClojureScript that provides its own equality semantics, where equality comparison of data structures are based on value and not of reference. So using the Object.is algorithm without an escape hatch is an issue. Similarly for setState. |
@gaearon Option 3 is simple and works well for my case but clashes with |
Give this man a medal! |
I think it is quite easy to compose a solution to this problem with one or two small hooks, I think. I just started using hooks, though, so perhaps my approach is somehow incorrect // Returns a number representing the "version" of current.
function useChangeId<T>(next: T, isEqual: (prev: T | undefined, next: T) => boolean) {
const prev = useRef<T>()
const id = useRef(0)
if (!isEqual(prev.current, next)) {
id.current++
}
useEffect(() => { prev.current = next }, [next])
return id
}
interface FetchConfig { /* ... etc ... */ }
function useFetch(config: FetchConfig) {
// Returns a new ID when config does not deep-equal previous config
const changeId = useChangeId(config, _.isEqual)
// Returns a new callback when changeId changes.
return useCallback(() => implementUseFetchHere(config), [changeId])
}
function MyComponent(props: { url: string }) {
const fetchConfig = { url, ... }
const fetch = useFetch(fetchConfig)
return <button onClick={fetch}>Fetch</button>
} |
Every time the useSendPageViewEvent() function was being called without the xdm and data params being specified, the default parameter would create a new object. This would make the function run again aka it was not safe to call multiple times because the data and xdm variables didn't pass the === test. Since the values are small, there isn't much worry about performance. See this comment about the subject: facebook/react#14476 (comment)
* Fix (most) eslint errors in sandbox * Value-compare data and xdm in useEffect dependency arrays Every time the useSendPageViewEvent() function was being called without the xdm and data params being specified, the default parameter would create a new object. This would make the function run again aka it was not safe to call multiple times because the data and xdm variables didn't pass the === test. Since the values are small, there isn't much worry about performance. See this comment about the subject: facebook/react#14476 (comment)
Currently we can pass an array as second argument when using
useCallback
oruseEffect
like below:The problem is it only compare array items with
===
, it there any way to compare complex object ?Support custom comparator as third argument looks not bad:
The text was updated successfully, but these errors were encountered: