shikher prasad
shikher prasad

Reputation: 1

useRef behavior is different in strict mode and non-strict mode in React 18 development

const { Fragment, StrictMode, useRef, useState } = React;
const { createRoot } = ReactDOM;

function CountLabel({ count }) {
  const prevCount = useRef(count);
  console.log("prevCount: ", prevCount.current)
  console.log("count: ", count)
  const [trend, setTrend] = useState(null);
  if (prevCount.current !== count) {
    setTrend(count > prevCount.current ? 'increasing' : 'decreasing');
    prevCount.current = count;
  }
  console.log("trend: ", trend)
  return (
    <Fragment>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </Fragment>
  );
}

function App() {
  const [count, setCount] = useState(0);
  return (
    <Fragment>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
      <CountLabel count={count} />
    </Fragment>
  );
}

const strictModeEnabled = createRoot(document.getElementById("strict-mode-enabled"));
strictModeEnabled.render(
  <StrictMode>
    <h1>Strict Mode Enabled</h1>
    <App />
  </StrictMode>
);

const strictModeDisabled = createRoot(document.getElementById("strict-mode-disabled"));
strictModeDisabled.render(
  <Fragment>
    <h1>Strict Mode Disabled</h1>
    <App />
  </Fragment>
);
#wrapper {
  display: flex;
}

#wrapper > div {
  margin: 16px;
}
<div id="wrapper">
  <div id="strict-mode-enabled"></div>
  <div id="strict-mode-disabled"></div>
</div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

The expectation is on click of Increment, the count should increment by 1 and trend should be increasing and opposite for on click of Decrement.

In strict and development mode, the count is increasing, but the trend is always null.

I know in strict mode React is calling this child component two times.

During first render, the logs are:

prevCount: 0
count: 0
trend: null
prevCount: 0
count: 0
trend: null

After Increment click, the logs are:

prevCount: 0
count: 1
trend: null
prevCount: 1
count: 1
trend: increasing 
prevCount: 1
count: 1
trend: null

But if strict mode is off, it works fine as expected. It also works fine if instead of useRef, we keep prevCount in state like below:

const { Fragment, StrictMode, useRef, useState } = React;
const { createRoot } = ReactDOM;

function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  console.log("prevCount: ", prevCount)
  console.log("count: ", count)
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  console.log("trend: ", trend)
  return (
    <Fragment>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </Fragment>
  );
}

function App() {
  const [count, setCount] = useState(0);
  return (
    <Fragment>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
      <CountLabel count={count} />
    </Fragment>
  );
}

const strictModeEnabled = createRoot(document.getElementById("strict-mode-enabled"));
strictModeEnabled.render(
  <StrictMode>
    <h1>Strict Mode Enabled</h1>
    <App />
  </StrictMode>
);

const strictModeDisabled = createRoot(document.getElementById("strict-mode-disabled"));
strictModeDisabled.render(
  <Fragment>
    <h1>Strict Mode Disabled</h1>
    <App />
  </Fragment>
);
#wrapper {
  display: flex;
}

#wrapper > div {
  margin: 16px;
}
<div id="wrapper">
  <div id="strict-mode-enabled"></div>
  <div id="strict-mode-disabled"></div>
</div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

The logs on first render is same as above, but this is how the logs are on click of increment button:

prevCount: 0
count: 1
trend: null
prevCount: 1
count: 1
trend: increasing 
prevCount: 0
count: 1
trend: null
prevCount: 1
count: 1
trend: increasing 

My doubt is, why using useRef is bad idea here? Works good in non-strict mode, but not good in strict mode?

Upvotes: 0

Views: 802

Answers (1)

Wing
Wing

Reputation: 9721

Refs and state both store information across renders however they're designed for different things:

Refs State
Store information across renders
Re-render component when data changes
Use data immediately after the data changes

The above table illustrates the benefit/trade-off when deciding between refs and state. If you use a ref, you can change the data and use it immediately (without waiting for the next render) however you lose the benefit of re-rendering the component. If you use state, you get the benefit of the component re-rendering in response to the state change however you can't use the data until the next render.

However, why is this? Why do I have to make a trade-off? Why can't I get the benefit of both worlds? This is down to a core point in React: keeping components pure. Taken from React's documentation on component purity, purity is:

In computer science (and especially the world of functional programming), a pure function is a function with the following characteristics:

  • It minds its own business. It does not change any objects or variables that existed before it was called.
  • Same inputs, same output. Given the same inputs, a pure function should always return the same result.

Purity is core because React uses it to decide when to re-render. If the inputs change, React knows to change the output. Inputs in React are props, state and context. For state, this means when it changes (e.g. calling the set function from useState), the new data becomes the input of the next render. As it's the input of the next render, you can't use it in the current one.

However, we sometimes want to use the data immediately, outside of React's render process. This is where refs come in. Refs allow you to use the changed data outside of React's render process but because it's outside this process, React can't use it as an input or output for renders.

This leads to your problem: you're using your ref as an input and output to your render.

function CountLabel({ count }) {
  const prevCount = useRef(count);

  // ...

  // Output (reading `prevCount.current`)
  if (prevCount.current !== count) {
    setTrend(count > prevCount.current ? 'increasing' : 'decreasing');

    // Input (writing `prevCount.current`)
    prevCount.current = count;
  }

  // ...

  return (...);
}

You might say "but it works in non-strict mode"! However, imagine this was in a more complex application where the component is called multiple times without the count prop changing. It would break in the way you're seeing when strict mode is enabled. And this is why strict mode exists: it calls components twice to help you find components that don't produce the same outputs when given the same inputs (impure components) before this bug reaches a production environment.

Further reading

Upvotes: 0

Related Questions