DEV Community

Cover image for You Are Using the “useState” Hook Wrong
Elson Correia
Elson Correia

Posted on • Updated on

You Are Using the “useState” Hook Wrong

React API is not developer friendly. One of them is the useState hook which is another misunderstood API much like useEffect .

It looks like this:

const [active, setActive] = useState(false);
Enter fullscreen mode Exit fullscreen mode

At first glance, it does not look like it is doing anything wrong but if you take a look at how React developers use it you may start to see why:

const [active, setActive] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [list, setList] = useState([]);
Enter fullscreen mode Exit fullscreen mode

The React documentation hints at the practice of individualizing the state into separate useState and the community went berzerk with it creating components with unnecessary re-renders for a slow app.

The useState hook API mimics the legacy class component state property and setState method which was already a bad API.

class Example extends React.Component {
   constructor(props) {
     super(props);

     this.state = {
       active: false,
       loading: true,
       error: null,
       list: [],
       user: null
     };
   }

   someAction = () => {
      this.setState({
        ...this.state, 
        active: true, 
        loading: false
      })
   }

   render() {
      return (
        ...
      );
    }
}
Enter fullscreen mode Exit fullscreen mode

React went from suggesting a grouped update to a single state update knowing that react performance is bad and that every state update is a component re-render (at least re-render calculation).

React already bathes state updates but this only works when inside React event handler. When we have asynchronous stuff which is very popular now, we are better off grouping state updates.

The previous this.setState was not friendly because it required spreading the current state or passing a callback which would give you access to the state and props that you can use in your state update.

this.setState((state, props) => ({
  ...state,
  active: props.isNew, 
  loading: false
}))
Enter fullscreen mode Exit fullscreen mode

Developers felt the pain when it came to the spreading state. Especially deep-state spreads.

this.setState((state, props) => ({
   ...state,
   list: [
      ...state.list,
      props.item
   ], 
   loading: false,
   user: {
      ...state.user,
      name: "John Doe"
   }
}))
Enter fullscreen mode Exit fullscreen mode

Deeply spreading the state was tough and that’s why lodash was popular and useful back then. It helped with it! Though it was still better than what we have today because it would only do a single state update which is good for your component.

The similar way to use the useState hook would be like this:

const [state, setState] = useState<State>({
   active: false,
   loading: true,
   error: null,
   list: [],
   user: null
});
Enter fullscreen mode Exit fullscreen mode

This is better for your app because you have the option to make grouped updates which will only re-render your component once instead of calling each action from useState . But you still need to spread the state, which is where the pain is.

setState({
   ...state,
   list: [
     ...state.list,
     props.item
   ], 
   loading: false,
   user: {
     ...state.user,
     name: "John Doe"
   }
})
Enter fullscreen mode Exit fullscreen mode

Thankfully, we have useReducer hook that we can use to solve this issue and minimize how many times our components have to re-render.

const [state, setState] = useReducer(
   (prev: State, curr:Partial<State>) => ({...prev, ...curr}), 
   {
     active: false,
     loading: true,
     error: null,
     list: [],
     user: null
});
Enter fullscreen mode Exit fullscreen mode

The above alternative allows us to avoid spreading and still solves group update needs. But it does not handle deep object merging requiring spreading the current state to update it. But just for deep objects.

setState({
   list: [
      ...state.list,
      props.item
   ], 
   loading: false,
   user: {
      ...state.user,
      name: "John Doe"
   }
})
Enter fullscreen mode Exit fullscreen mode

So let’s improve this by using a deep object merge utility function:

const [state, setState] = useReducer(
   (prev: State, curr:Partial<State>) => mergeObjects(prev, curr), 
   {
     active: false,
     loading: true,
     error: null,
     list: []
     user: null
   }
);
Enter fullscreen mode Exit fullscreen mode

Where mergeObjects can be custom or you can use lodash merge utility function:

function mergeObjects(a, b) {
   if(a === null || typeof a !== 'object') return b;
   if(b === null || typeof b !== 'object') return b;

   const obj = Array.isArray(a) ? [...a] : a;

   for(const key in b) {
     if(b.hasOwnProperty(key)) {
       obj[key] = mergeObjects(obj[key], b[key]);
     }
   }

   return obj;
}
Enter fullscreen mode Exit fullscreen mode

We can now abstract all this into a new hook:

const useCompState = <S>(initialState) => {
   const [state, setState] = useReducer(
     (prev: S, curr:Partial<S>) => mergeObjects(prev, curr), 
     initialState);

   return {state, setState}
}
Enter fullscreen mode Exit fullscreen mode

And then use it like this:

const {state, setState} = useCompState<State>({
   active: false,
   loading: true,
   error: null,
   list: [],
   user: null
});

// grouped update
setState({
   list: [
     ...state.list,
     props.item
   ], 
   loading: false,
   user: {
     name: "John Doe"
   }
})

// single update
setState({
   loading: false
});
Enter fullscreen mode Exit fullscreen mode

In the above, I only had to spread the array because it's adding an item to the end, but any deep object value would be automatically handled easily.

Isn't deep-state copy expensive?

It is! This is why it's best to keep the state simple. On the other hand, in modern computers and Javascript engines, the processing power required is next to nothing. Unless you clearly have a performance issue, this should not be an issue.

Take Away

The mutable nature of the state is probably the worse part, unfortunately not one we can do much about without introducing significant API changes. However, we can improve how we interact with state and props data inside our components.

When handling a state like this with a Typescript project, having a State interface to represent the internal data of your component gives you a nice view of what the component deals with in a single place. It’s much better to maintain the component that way.

The grouped update is always better because it reduces the re-renders which helps avoid weird errors and behaviors of components where React batch updates don't work. Not to mention performance improvements.

Let me know what you think in the comments…

YouTube Channel: Before Semicolon
Website: beforesemicolon.com

Top comments (4)

Collapse
 
vezyank profile image
Slava

This post is completely wrong. The author would know this if he read the React docs.

More state is always better than bigger state. Deeply copying objects is one of the most expensive operations available.

Renders from multiple state updates are auto-batched. You're using "usestate" right.

Collapse
 
ecorreia profile image
Elson Correia

Thanks for the input. Appreciated!

  • React only batches synchronous state updates which happens inside the event handler. This means that state updates that happen inside asynchronous actions won't get batched and taking that asynchronous actions are super popular and used, nobody is benefiting from state batches. Regardless, grouping state updates will have more benefits in both scenarios;

  • Deep copying and spreading object performance for state of this size is so negletable when it comes to modern computer and JS engines that you rather worry about the costly re-render nature of React.

Additional benefits (not in the article)

  • the "useCompState" can be expended to perform state checks (guard data) which often times is done using more states.

  • reduce the need to manage various hooks. combine them into 1.

Don't like the deep copy? use native spread operator. It does not matter!

Collapse
 
vezyank profile image
Slava

I suggest you read the official React documentation. You have many misconceptions about its performance profile.

Thread Thread
 
ecorreia profile image
Elson Correia

Thats pretty broad. Any particular place you can share the links to?