Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutating state from React's useState hook

Is it, and why is it a bad idea to mutate a state from React's new useState hook? I found no info on the topic.

Consider following code:

const [values, setValues] = useState({})

// doSomething can be called once, or multiple times per render

const doSomething = (name, value) => {
  values[name] = value
  setValues({ ...values })
}

Note the mutation of values. Since doSomething can be called more than once per render, doing this would not work because of the async properties of setState:

const doSomething = (name, value) => {
  setValues({ ...values, [name]: value })
}

Is the approach of mutating values directly the correct one in this case?

like image 366
Mykybo Avatar asked Mar 15 '19 08:03

Mykybo


2 Answers

You should never mutate state directly as it might not even cause a re-render if you update the state with the same object reference.

const { useState } = React;

function App() {
  const [values, setValues] = useState({});

  const doSomething = (name, value) => {
    values[name] = value;
    setValues(values);
  };

  return (
    <div onClick={() => doSomething(Math.random(), Math.random())}>
      {JSON.stringify(values)}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

You can give a function as first argument to setValues just like you are used to in the class component setState, and that function in turn will get the correct state as argument, and what is returned will be the new state.

const doSomething = (name, value) => {
  setValues(values => ({ ...values, [name]: value }))
}

const { useState } = React;

function App() {
  const [values, setValues] = useState({});

  const doSomething = (name, value) => {
    setValues(values => ({ ...values, [name]: value }));
  };

  return (
    <div onClick={() => doSomething(Math.random(), Math.random())}>
      {JSON.stringify(values)}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
like image 51
Tholle Avatar answered Sep 28 '22 18:09

Tholle


Basically I would avoid mutating state in such way simply for the sake of purity.

However, I would argue that in this case it is perfectly fine. When you mutate a value in an inner level in the state it goes unnoticed by React. Only when calling setValues() with a new reference React notes to itself that a new render is pending.

const { useState } = React;

function App() {
  const [values, setValues] = useState({ num: 0 });

  const handleClick = () => {
    doSomething();
    doSomething();
  }
  
  const doSomething = () =>
    setValues((values) => {
      values.num += 1;
      return { ...values };
    });

return (
  <div onClick={handleClick}>
    {JSON.stringify(values)}
  </div>
);
}

ReactDOM.render( < App / > , document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

(If anyone can provide a counter example I would love to see it).

like image 26
Erez Cohen Avatar answered Sep 28 '22 18:09

Erez Cohen