3

Let's take following example with an inline reducer:

const App = () => {
  const [state, dispatch] = React.useReducer(s => {
    console.log("reducer invoked with state", s)
    return s + 1
  }, 0);

  return (
    <div>
      <p>{state}</p>
      <button onClick={dispatch}>Increment</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

On first click, App is rendered twice, subsequent clicks cause only one re-render.

I would like to understand above behavior of useReducer. After having read this answer, some things are still not clear to me.

  1. Why is a double render triggered only first time for useReducer?
  2. Are there additional circumstances to 1.), which cause double rendering for an inline reducer as well?
  3. When would this inline reducer pattern be not recommended (performance, etc.)? Or is it without flaws?
bela53
  • 3,040
  • 12
  • 27

3 Answers3

1

By putting the reducer function inside the component, you make it so each time the component renders a new function is created. It may have the same text, but react can't tell that, so it's forced to run the old and new functions and compare their results.

In the following snippet i've made the reducer function log out which instance of the function is running, and you'll see that those first two double-calls are two different functions. I'm not sure why it doesn't need to also do this on later times

let count = 0;
const App = () => {
  count++;
  const [state, dispatch] = React.useReducer(s => {
    console.log(`reducer #${count} invoked with state`, s)
    return s + 1
  }, 0);

  return (
    <div>
      <p>{state}</p>
      <button onClick={dispatch}>Increment</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

So yes, using an inline reducer can reducer performance a little bit. As a result, if you don't need to inline it, then you should move it outside the component.

If you do need to inline it (say, because you need to refer to other variables inside the component), it's probably not a big deal in most cases. If it is, then you can useCallback to memoize it and only change it when needed.

const Example = () => {
  const [size, setSize] = useState(1);
  const reduction = useCallback((s) => {
    console.log(`reducer called; something = ${something}`);
    return s + size;
  }, [something]l
  const [state, dispatch] = useReducer(reduction, 0);
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • Thanks for the answer! I am still curious about a more in-depth explanation to question 1 and 2. – bela53 May 21 '20 at 18:16
0

I cannot find an issue with an inline reducer.

In this extended example we're not seeing reducers re-evaluate on render as demonstrate by the interval'd value change.

In any event, these inlined method are optimized away by V8 and JavaScriptCore — the cost is not much, this was an area investigated by the React core team before introducing hooks.

const App = () => {
  const [random, setRandom] = React.useReducer(() => {
    const value = Math.random()
    console.log("random value reducer", value)
    return value;
  }, 0)
  React.useEffect(() => {
    const id = setInterval(() => setRandom(), 1000)
    return () => clearInterval(id)
  }, [])

  const [state, dispatch] = React.useReducer(s => {
    console.log("reducer invoked with state", s, random)
    return s + 1
  }, 0);

  return (
    <div id={random}>
      <button onClick={dispatch}>Increment</button>
      &nbsp;
      {state}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
evanrs
  • 96
  • 5
0

Q: Why is a double render triggered only first time for useReducer?

A: In React 18 with strict mode activated in development, components mount, then unmount and mount again. This is probably why you see the double trigger only first time.

Q: Are there additional circumstances to 1.), which cause double rendering for an inline reducer as well?

A: I would guess useReducer is similar to useState. If you supply a different value to useState on the second render, it won't change state.

Q: When would this inline reducer pattern be not recommended (performance, etc.)? Or is it without flaws?

A: I don't know.

Gøran Cantona
  • 611
  • 4
  • 5