React: Bug: Derived value does not update in StrictMode

Created on 9 Feb 2020  路  3Comments  路  Source: facebook/react

React version:
16.12.0

Steps To Reproduce

  1. Enter a valid number in the textbox that uses strict mode. Notice the value changes in the textbox that does not use strict mode
  2. Enter a valid number in the textbox that does not use strict mode. Notice the value does not change in the textbox that uses strict mode

Link to code example:
https://codesandbox.io/s/elated-mendeleev-1m5tm

React App

The current behavior

Derived value does not update in strict mode

The expected behavior

Derived value should update in strict mode

Unconfirmed

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:

  1. "loose" input changes e.g. 1 -> 2
  2. "strict" input renders for the first time

    1. It changes prevValue ref from 1 -> 2 (side effect) and also updates state setInternalValue("")

    2. It immediately re-renders with the newer state value (internalValue 1 -> 2)

  3. React resets component state (including the update setInternalValue("")) in preparation for the second render. (Remember, StrictMode renders twice.)
  4. "strict" input renders for the second time

    1. Since the ref was mutated during the first render, it's already the same as the "new" prop so the code you expect to update the 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:

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?

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.)

All 3 comments

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.

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:

  1. "loose" input changes e.g. 1 -> 2
  2. "strict" input renders for the first time

    1. It changes prevValue ref from 1 -> 2 (side effect) and also updates state setInternalValue("")

    2. It immediately re-renders with the newer state value (internalValue 1 -> 2)

  3. React resets component state (including the update setInternalValue("")) in preparation for the second render. (Remember, StrictMode renders twice.)
  4. "strict" input renders for the second time

    1. Since the ref was mutated during the first render, it's already the same as the "new" prop so the code you expect to update the 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:

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?

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.)

Thank you @bvaughn for the quick reply and detailed explanation.

Was this page helpful?
0 / 5 - 0 ratings