DEV Community

Cover image for Avoid These 6 Common React useState Hook Mistakes (Code Example)
Akshay Kaushik
Akshay Kaushik

Posted on • Originally published at blog.akshaykaushik.eu.org

Avoid These 6 Common React useState Hook Mistakes (Code Example)

As a React developer, you're probably familiar with the UseState hook. It's a powerful tool that allows you to add state to your functional components. However, it's easy to make mistakes when using it. In this article, we'll explore the most common mistakes and how to avoid them.

Let's walk through the 6 most common useState mistakes, ranging from conceptual errors to subtle syntax issues, and learn how to avoid them through clear examples. Understanding these common pitfalls will level up your React skills.

1. Not Understanding How useState Works

The useState hook returns an array with 2 elements. The first element is the current state value, while the second element is a state setter function.

It's important to understand that useState does not merge object state updates - it completely overwrites state.

View this example:

import React, {useState} from 'react';

function MyComponent() {
  const [state, setState] = useState({
    name: 'John',
    age: 20 
  });

  const handleUpdate = () => {
    // This will overwrite existing state 
    setState({
      name: 'Sam' 
    });
  }

  return (
    <div>
      {/*Renders 'Sam' instead of 'John' after handleUpdate call*/}  
      <h1>{state.name}</h1> 
      <h2>{state.age}</h2>

      <button onClick={handleUpdate}>Update Name</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

To merge state updates correctly, pass an updater function to setState:

setState(prevState => {
  return {...prevState, name: 'Sam'} 
});
Enter fullscreen mode Exit fullscreen mode

2. Passing Functions Inline

It's convenient to pass functions inline to event handlers or effects, but doing this breaks updating state:

import {useState, useEffect} from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // Closures prevent this from working
    document.addEventListener('click', () => setCount(count + 1));
  }, []);

  return <h1>{count}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Since function closures are created on each render, count never updates properly.

Pass functions inline only if they don't update state.

Instead, declare them outside or in useCallback():

// Outside
function increment() {
  setCount(count + 1);
}

// In useCallback() 
const increment = useCallback(() => {
  setCount(count + 1); 
}, []);
Enter fullscreen mode Exit fullscreen mode

3. Forgetting to Handle Arrays and Objects Correctly

Another mistake is mutating state arrays or objects directly:

const [tags, setTags] = useState(['react', 'javascript']);

const addTag = () => {
  // Mutates existing array 
  tags.push('nodejs');  
}

return <div>{tags}</div>
Enter fullscreen mode Exit fullscreen mode

Since the tags array was mutated directly, React can't detect changes and won't trigger re-renders properly.

Instead, create a new copy of array/object before passing to setter function:

const addTag = () => {
  setTags([...tags, 'nodejs']);  
}
Enter fullscreen mode Exit fullscreen mode

Spread syntax [...array] shallow copies the array. This lets React detect changes.

4. Making Unnecessary useState Calls

There's no need to separate every single field or data point into its own state hook. Doing so hurts performance unnecessarily:

function MyForm() {
  const [firstName, setFirstName] = useState(''); 
  const [lastName, setLastName] = useState('');

  return (
    <>
      <input 
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
      />

      <input
        value={lastName}  
        onChange={(e) => setLastName(e.target.value)} 
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Instead, group related state into single objects:

function MyForm() {
  const [fields, setFields] = useState({
    firstName: '',
    lastName: '' 
  })

  // Call once to update both name fields
  const handleChange = (e) => {
    setFields({
      ...fields,
      [e.target.name]: e.target.value 
    })
  }

  return (
    <>
      <input 
        name="firstName"
        value={fields.firstName}
        onChange={handleChange} 
      />

      <input 
        name="lastName"
        value={fields.lastName}
        onChange={handleChange}
      /> 
    </>
  );  
}
Enter fullscreen mode Exit fullscreen mode

5. Incorrect State Comparison

When dealing with objects or arrays, avoid direct comparisons for state changes.

// Incorrect
if (user !== newUser) {
  // ...

// Correct
if (user.id !== newUser.id) {
  // ...
Enter fullscreen mode Exit fullscreen mode

6. Not Setting a Proper Initial State

It's important to initialize state properly:

1. Initialize based on props

Rather than hard-coding values:

// Hard-coded
const [count, setCount] = useState(0); 

// Initialise based on props
const [count, setCount] = useState(props.initialCount);
Enter fullscreen mode Exit fullscreen mode

2. Handle unloaded data

If fetching data in useEffect, set loading state:

const [user, setUser] = useState(null);

useEffect(() => {
  fetchUser().then(user => setUser(user))  
}, []);

// Check for null before rendering
return <div>{user ? user.name : 'Loading...'}</div>
Enter fullscreen mode Exit fullscreen mode

This avoids errors from rendering undefined state.

3. Specify data types for state

Typing state helps avoid issues:

const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<{name: string} | null>(null);
Enter fullscreen mode Exit fullscreen mode

This ensures you prevent passing an invalid updated state value to setter functions.

Bookmark this as a handy useState troubleshooting guide. And most importantly, pay special attention to problem areas like spreading arrays before setting state, not mutating objects directly, understanding how useState batching.

Mastering useState fundamentals takes your React game to the next level!

Frequently Asked Questions

Q: Why call a function to update state instead of changing it directly?

A: Calling the setter function lets React trigger re-renders. It also queues up state changes for more reliable batching.

Q: How do you update object state properly?

A: Either replace the state completely by passing a new object, or shallow merge by spreading the previous state. Never directly mutate state objects.

Q: When should useCallback be used with state setters?

A: Use it if passing the setter down in a callback or effect to prevent unnecessary recreations between renders.

Q: What's the difference between passing a value vs updater function to setState?

A: Value replaces state wholly while updater merges by taking previous state as first argument of function.

Q: Why spread arrays/objects before updating state?

A: Spreading creates a copied version React can watch for changes, avoiding issues with mutation.

I require immediate work. Entry-level is also acceptable.

Top comments (0)