use-force-update
use-force-update copied to clipboard
Proposed Improvements
Current Code
export default function useForceUpdate(): () => void {
const [ , dispatch ] = useState<{}>(Object.create(null));
// Turn dispatch(required_parameter) into dispatch().
const memoizedDispatch = useCallback(
(): void => {
dispatch(Object.create(null));
},
[ dispatch ],
);
return memoizedDispatch;
}
Proposed Improvements
JS, but can easily be typed:
import { useRef, useState } from 'react';
// Creates an empty object, but one that doesn't inherent from Object.prototype
const newValue = () => Object.create(null);
export default () => {
const setState = useState(newValue())[1];
const forceUpdate = useRef(() => {
setState(newValue());
}).current;
return forceUpdate;
};
Rational:
First, dispatch
unclear. Looking at the history, it is a residue from when useReducer
was used, but this is no longer the case.
Then, from the docs (see note):
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
So there's no need providing it as a useCallback
dependency.
Either useCallback
or useMemo
with empty dependencies ([]
- ie, one-off) is identical to useRef().current
which somehow communicates the intent better - this value will not change. Also, with useRef
, react doesn't call Object.is(old, new) each render.
Also note that in the current code we have memoizedDispatch
but because dispatch
is stable, there's no real memoization going on - useCallback
will always return the same function.
Finally, in my case, initial render could trigger dozens of forceUpdates
so a further improvement could be (I'm not actually using it because the trade-off doesn't seem to be worth it, but others may find this useful):
import { useRef, useState, useEffect } from 'react';
// Creates an empty object, but one that doesn't inherent from Object.prototype
const newValue = () => Object.create(null);
export default () => {
const setState = useState(newValue())[1];
const updatePending = useRef(false);
const forceUpdate = useRef(() => {
if (!updatePending.current) {
setState(newValue());
updatePending.current = true;
}
}).current;
useEffect(() => {
updatePending.current = false;
});
return forceUpdate;
};
I like renaming dispatch
and dropping it from the dependency array.
Calling forceUpdate
twice resulting in two updates is by design. I did not want to hinder a developer's ability to do that if they wanted, for whatever reason. If you call forceUpdate twice synchronously, you get two re-renders. The only reason I would want to change this is if class components' this.forceUpdate
de-dupes. I'm not positive that it does, but since this hook is meant to replace that method, it would be the definitive source for behavior.
Can you elaborate more on if there is real benefit to useRef over useCallback? This was a discussion before, but there was no justification for useRef, while useCallback matched the exact documentation for use case -- memoizing a function. In this case, we are memoizing the removal of a parameter. The idea is that for dependency-less memoization, useRef avoids comparisons? Is there ever a situation where a dependency-less useMemo or useCallback would be a better choice than useRef? For readability or for performance?
A few things:
I did not want to hinder a developer's ability to do that if they wanted, for whatever reason.
I agree with this decision. I think it is based on assumptions; it clutters the code; and its benefit depends on the use-case. As I mentioned, I don't even use this myself.
If you call forceUpdate twice synchronously, you get two re-renders.
This is not the case. Within a render cycle, once you call setState
in a component, react simply renders it as 'dirty', which will queue a new render-cycle (at the end of the current one) where any dirty component will be re-rendered. But you can so long you are within the same render cycle, you can call setState
1000 times on the same component - it will still only re-render once. (Also, worth remembering that only the value in the last setState
is used, and that within the same render cycle setState
doesn't actually modify the state. Rather, it queues the update).
...while useCallback matched the exact documentation for use case -- memoizing a function
As I said, this is not memoizing per-se. The idea with memoizing is that if the input doesn't change you simply return the previous computed value (and if the input changes, you re-compute; save it; return the value). In your case the input value ([]
) never changes other than in the first ever call.
The idea is that for dependency-less memoization, useRef avoids comparisons?
Both useMemo
and useCallback
still call Object.is
to compare the previous and current dependencies (where the previous []
and the current []
will always return "no change". I'm pretty sure this is the line of code where this happens.
As stated in the docs:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
It is exactly the same mechanism with useMemo
and useCallback
.
useRef
on the other hand means - always return the same value, always. So you always get the same ref
and you can mutate ref.current
at will. There are no dependencies involved here.
Also, and although hardly relevant, when you have:
const x = useMemo(()=> {...}, []);
You still allocation a new []
in each render.
Is there ever a situation where a dependency-less useMemo or useCallback would be a better choice than useRef?
My instinct is to say that there isn't such a situation, but I may have overlooked some cases? I took it to the SO community
This is not the case. Within a render cycle, once you call setState in a component, react simply renders it as 'dirty', which will queue a new render-cycle (at the end of the current one) where any dirty component will be re-rendered. But you can so long you are within the same render cycle, you can call setState 1000 times on the same component - it will still only re-render once.
There is no guarantee that two calls to forceUpdate
will be within the same render cycle. React re-renders synchronously. Try it!
This is why unstable__batchedUpdates
was introduced. Otherwise, setState1(); setState2();
would trigger 2 re-renders.
I'll try to stay up-to-date with the SO post. I've never tried before today, but there doesn't appear to be a "subscribe" feature.
To be honest, I just came across the React implementation for this, which I reckon is superior to the proposed above:
const [, forceUpdate] = useReducer(x => x + 1, 0);
To be honest, I just came across the React implementation for this, which I reckon is superior to the proposed above:
This was the original implementation in this package, and it was requested to be changed so as not to have to deal with max integer values in [primarily older] browsers with a cap.
const [, forceUpdate] = useReducer(x => (x + 1) % Number.MAX_VALUE, 0);
?
See the initial commit.
It was changed to be simpler to read and more performant than addition and modulo per call, much the way ref would reduce an Object.is
comparison per call.
Benchmarks:
x = (x + 1) % Number.MAX_VALUE;
6,842,819 ops/s ±0.89%
98.99% slower
x = Object.create(null)
675,664,884 ops/s ±0.66%
fastest
Yet the slow option takes 6.8 microseconds to run - not something that needs optimising.
Another option:
const [, forceUpdate] = useReducer(x => Object.create(null));
I am fond of the fact that a library that can amount to being a single line long has nothing more to worry about than micro-optimization. It's why useCallback(f, [])
versus useRef(f).current
is so interesting.
Are you aware of any performance test suites that apply to React components?
@Izhaki another one for you:
const forceUpdate = useReducer(() => Symbol())[1];
@evoyy Symbol has poor browser support. As much as I dislike IE, I wouldn't want to cut out its users from all dependents on this package.