React version:
16.12.0
Link to code example:
https://codesandbox.io/s/elated-mendeleev-1m5tm
Derived value does not update in strict mode
Derived value should update in strict mode
FWIW it works fine with the production build of react-dom, change the import in codesandbox to test it:
import ReactDOM from "react-dom/cjs/react-dom.production.min";
When StrictMode
triggers different behavior, it's usually a sign of a side effect or an unsafe pattern that maybe just happens to work in most cases.
StrictMode
intentionally tests for this by double-invoking render methods. This means that, in DEV mode, React will render a component once- throw away any state updates- and then render again. (Regardless of how many times a component is rendered, the observable behavior should be the same unless there are side effects.)
In this particular case, mutating a ref during render is the side effect. The way this plays out in practice is like so:
prevValue
ref from 1 -> 2 (side effect) and also updates state setInternalValue("")
internalValue
1 -> 2)setInternalValue("")
) in preparation for the second render. (Remember, StrictMode
renders twice.)internalValue
state doesn't get called.The "quick fix" here would be to get rid of the render-phase side effect by moving your ref update to an effect:
if (prevValue.current !== value && internalValue !== "") {
setInternalValue("");
}
useEffect(() => {
// It's safe to update your prev-value ref here
prevValue.current = value;
});
However, this Code Sandbox also looks like one of the derived state anti-patterns described in a blog post a while back. Maybe worth re-considering the higher level approach being used here and possibly going with a fully-controlled component instead?
It might not be obvious why this matters. After all- if React didn't double-render the component, it wouldn't break. That's why using the production bundle "fixes" things.
The truth is that side effects like this might cause things to break even outside of strict mode. The upcoming "concurrent" rendering mode is one such case, but it's not the only one. A component might render twice between commits even in "legacy" (synchronous) rendering mode in the event of an error. (In that case, React would bubble the error up to the nearest error boundary, then re-render before committing.)
Thank you @bvaughn for the quick reply and detailed explanation.
Most helpful comment
When
StrictMode
triggers different behavior, it's usually a sign of a side effect or an unsafe pattern that maybe just happens to work in most cases.What went wrong here?
StrictMode
intentionally tests for this by double-invoking render methods. This means that, in DEV mode, React will render a component once- throw away any state updates- and then render again. (Regardless of how many times a component is rendered, the observable behavior should be the same unless there are side effects.)In this particular case, mutating a ref during render is the side effect. The way this plays out in practice is like so:
prevValue
ref from 1 -> 2 (side effect) and also updates statesetInternalValue("")
internalValue
1 -> 2)setInternalValue("")
) in preparation for the second render. (Remember,StrictMode
renders twice.)internalValue
state doesn't get called.How can you fix it?
The "quick fix" here would be to get rid of the render-phase side effect by moving your ref update to an effect:
However, this Code Sandbox also looks like one of the derived state anti-patterns described in a blog post a while back. Maybe worth re-considering the higher level approach being used here and possibly going with a fully-controlled component instead?
Why does this matter?
It might not be obvious why this matters. After all- if React didn't double-render the component, it wouldn't break. That's why using the production bundle "fixes" things.
The truth is that side effects like this might cause things to break even outside of strict mode. The upcoming "concurrent" rendering mode is one such case, but it's not the only one. A component might render twice between commits even in "legacy" (synchronous) rendering mode in the event of an error. (In that case, React would bubble the error up to the nearest error boundary, then re-render before committing.)