DEV Community 👩‍💻👨‍💻

Cover image for 5 Most Common useState Mistakes React Developers Often Make
Necati Özmen for refine

Posted on • Updated on • Originally published at refine.dev

5 Most Common useState Mistakes React Developers Often Make

Introduction

The most challenging aspect of developing any application is often managing its state. However, we are often required to manage several pieces of state in our application, such as when data is retrieved from an external server or updated in the app.

The difficulty of state management is the reason why so many state management libraries exist today - and more are still being developed. Thankfully, React has several built-in solutions in the form of hooks for state management, which makes managing states in React easier.

It's no secret that hooks have become increasingly crucial in React component development, particularly in functional components, as they have entirely replaced the need for class-based components, which were the conventional way to manage stateful components. The useState hook is one of many hooks introduced in React, but although the useState hook has been around for a few years now, developers are still prone to making common mistakes due to inadequate understanding.

The useState hook can be tricky to understand, especially for newer React developers or those migrating from class-based components to functional components. In this guide, we'll explore the top 5 common useState mistakes that React developers often make and how you can avoid them.

Steps we'll cover:

Initializing useState Wrongly

Initiating the useState hook incorrectly is one of the most common mistakes developers make when utilizing it. What does it mean to initialize useState? To initialize something is to set its initial value.

The problem is that useState allows you to define its initial state using anything you want. However, what no one tells you outright is that depending on what your component is expecting in that state, using a wrong date type value to initialize your useState can lead to unexpected behavior in your app, such as failure to render the UI, resulting in a blank screen error.

For example, we have a component expecting a user object state containing the user's name, image, and bio - and in this component, we are rendering the user's properties.

Initializing useState with a different data type, such as an empty state or a null value, would result in a blank page error, as shown below.

import { useState } from "react";

function App() {
  // Initializing state
  const [user, setUser] = useState();

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.name}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Output:

blank

Inspecting the console would throw a similar error as shown below:

Image error

Newer developers often make this mistake when initializing their state, especially when fetching data from a server or database, as the retrieved data is expected to update the state with the actual user object. However, this is bad practice and could lead to expected behavior, as shown above.

The preferred way to initialize useState is to pass it the expected data type to avoid potential blank page errors. For example, an empty object, as shown below, could be passed to the state:

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({});

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.name}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Output:

fix blank
We could take this a notch further by defining the user object's expected properties when initializing the state.

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({
    image: "",
    name: "",
    bio: "",
  });

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.name}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode


github support banner

Not Using Optional Chaining

Sometimes just initializing the useState with the expected data type is often not enough to prevent the unexpected blank page error. This is especially true when trying to access the property of a deeply nested object buried deep inside a chain of related objects.

You typically try to access this object by chaining through related objects using the dot (.) chaining operator, e.g., user.names.firstName. However, we have a problem if any chained objects or properties are missing. The page will break, and the user will get a blank page error.

For Example:

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({});

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.names.firstName}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Output error:

chain error

A typical solution to this error and UI not rendering is using conditional checks to validate the state's existence to check if it is accessible before rendering the component, e.g., user.names && user.names.firstName, which only evaluates the right expression if the left expression is true (if the user.names exist). However, this solution is a messy one as it would require several checks for each object chain.

Using the optional chaining operator (?.), you can read the value of a property that is buried deep inside a related chain of objects without needing to verify that each referenced object is valid. The optional chaining operator (?.) is just like the dot chaining operator (.), except that if the referenced object or property is missing (i.e., null or undefined), the expression short-circuits and returns a value of undefined. In simpler terms, if any chained object is missing, it doesn't continue with the chain operation (short-circuits).

For example, user?.names?.firstName would not throw any error or break the page because once it detects that the user or names object is missing, it immediately terminates the operation.

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({});

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user?.names?.firstName}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Taking advantage of the optional chaining operator can simplify and shorten the expressions when accessing chained properties in the state, which can be very useful when exploring the content of objects whose reference may not be known beforehand.

Updating useState Directly

The lack of proper understanding of how React schedules and updates state can easily lead to bugs in updating the state of an application. When using useState, we typically define a state and directly update the state using the set state function.

For example, we create a count state and a handler function attached to a button that adds one (+1) to the state when clicked:

import { useState } from "react";

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

  // Directly update state
  const increase = () => setCount(count + 1);

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={increase}>Add +1</button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The output:

directly

This works as expected. However, directly updating the state is a bad practice that could lead to potential bugs when dealing with a live application that several users use. Why? Because contrary to what you may think, React doesn't update the state immediately when the button is clicked, as shown in the example demo. Instead, React takes a snapshot of the current state and schedules this Update (+1) to be made later for performance gains - this happens in milliseconds, so it is not noticeable to the human eyes. However, while the scheduled Update is still in pending transition, the current state may be changed by something else (such as multiple users' cases). The scheduled Update would have no way of knowing about this new event because it only has a record of the state snapshot it took when the button got clicked.

This could result in major bugs and weird behavior in your application. Let’s see this in action by adding another button that asynchronously updates the count state after a 2 seconds delay.

import { useState } from "react";

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

  // Directly update state
  const update = () => setCount(count + 1);

  // Directly update state after 2 sec
  const asyncUpdate = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 2000);
  };

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={update}>Add +1</button>
      <button onClick={asyncUpdate}>Add +1 later</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pay attention to the bug in the output:

attention

Notice the bug? We start by clicking on the first "Add +1" button twice (which updates the state to 1 + 1 = 2). After which, we click on the "Add +1 later" – this takes a snapshot of the current state (2) and schedules an update to add 1 to that state after two seconds. But while this scheduled update is still in transition, we went ahead to click on the "Add +1" button thrice, updating the current state to 5 (i.e., 2 + 1 + 1 + 1 = 5). However, the asynchronous scheduled Update tries to update the state after two seconds using the snapshot (2) it has in memory (i.e., 2 + 1 = 3), not realizing that the current state has been updated to 5. As a result, the state is updated to 3 instead of 6.

This unintentional bug often plagues applications whose states are directly updated using just the setState(newValue) function. The suggested way of updating useState is by functional update which to pass setState() a callback function and in this callback function we pass the current state at that instance e.g., setState(currentState => currentState + newValue). This passes the current state at the scheduled update time to the callback function, making it possible to know the current state before attempting an update.

So, let's modify the example demo to use a functional update instead of a direct update.

import { useState } from "react";

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

  // Directly update state
  const update = () => setCount(count + 1);

  // Directly update state after 3 sec
  const asyncUpdate = () => {
    setTimeout(() => {
      setCount((currentCount) => currentCount + 1);
    }, 2000);
  };

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={update}>Add +1</button>
      <button onClick={asyncUpdate}>Add +1 later</button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Output:
attention two

With functional update, the setState() function knows the state has been updated to 5, so it updates the state to 6.

Updating Specific Object Property

Another common mistake is modifying just the property of an object or array instead of the reference itself.

For example, we initialize a user object with a defined "name" and "age" property. However, our component has a button that attempts to update just the user's name, as shown below.

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
  });

  // Update property of user state
  const changeName = () => {
    setUser((user) => (user.name = "Mark"));
  };

  // Render UI
  return (
    <div className='App'>
      <p>User: {user.name}</p>
      <p>Age: {user.age}</p>

      <button onClick={changeName}>Change name</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Initial state before the button is clicked:

Image initial

Updated state after the button is clicked:

updated state

As you can see, instead of the specific property getting modified, the user is no longer an object but has been overwritten to the string “Mark”. Why? Because setState() assigns whatever value returned or passed to it as the new state. This mistake is common with React developers migrating from class-based components to functional components as they are used to updating state using this.state.user.property = newValue in class-based components.

One typical old-school way of doing this is by creating a new object reference and assigning the previous user object to it, with the user’s name directly modified.

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
  });

  // Update property of user state
  const changeName = () => {
    setUser((user) => Object.assign({}, user, { name: "Mark" }));
  };

  // Render UI
  return (
    <div className='App'>
      <p>User: {user.name}</p>
      <p>Age: {user.age}</p>

      <button onClick={changeName}>Change name</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Updated state after the button is clicked:

state assignment

Notice that just the user’s name has been modified, while the other property remains the same.

However, the ideal and modern way of updating a specific property or an object or array is the use of the ES6 spread operator (...). It is the ideal way to update a specific property of an object or array when working with a state in functional components. With this spread operator, you can easily unpack the properties of an existing item into a new item and, at the same time, modify or add new properties to the unpacked item.

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
  });

  // Update property of user state using spread operator
  const changeName = () => {
    setUser((user) => ({ ...user, name: "Mark" }));
  };

  // Render UI
  return (
    <div className='App'>
      <p>User: {user.name}</p>
      <p>Age: {user.age}</p>

      <button onClick={changeName}>Change name</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The result would be the same as the last state. Once the button is clicked, the name property is updated while the rest of the user properties remain the same.

Managing Multiple Input Fields in Forms

Managing several controlled inputs in a form is typically done by manually creating multiple useState() functions for each input field and binding each to the corresponding input field. For example:

import { useState, useEffect } from "react";

export default function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [age, setAge] = useState("");
  const [userName, setUserName] = useState("");
  const [password, setPassword] = useState("");
  const [email, setEmail] = useState("");

  // Render UI
  return (
    <div className='App'>
      <form>
        <input type='text' placeholder='First Name' />
        <input type='text' placeholder='Last Name' />
        <input type='number' placeholder='Age' />
        <input type='text' placeholder='Username' />
        <input type='password' placeholder='Password' />
        <input type='email' placeholder='email' />
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Furthermore, you have to create a handler function for each of these inputs to establish a bidirectional flow of data that updates each state when an input value is entered. This can be rather redundant and time-consuming as it involves writing a lot of code that reduces the readability of your codebase.

However, it's possible to manage multiple input fields in a form using only one useState hook. This can be accomplished by first giving each input field a unique name and then creating one useState() function that is initialized with properties that bear identical names to those of the input fields.

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    firstName: "",
    lastName: "",
    age: "",
    username: "",
    password: "",
    email: "",
  });

  // Render UI
  return (
    <div className='App'>
      <form>
        <input type='text' name='firstName' placeholder='First Name' />
        <input type='text' name='lastName' placeholder='Last Name' />
        <input type='number' name='age' placeholder='Age' />
        <input type='text' name='username' placeholder='Username' />
        <input type='password' name='password' placeholder='Password' />
        <input type='email' name='email' placeholder='email' />
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After which, we create a handler event function that updates the specific property of the user object to reflect changes in the form whenever a user types in something. This can be accomplished using the spread operator and dynamically accessing the name of the specific input element that fired the handler function using the event.target.elementsName = event.target.value.

In other words, we check the event object that is usually passed to an event function for the target elements name (which is the same as the property name in the user state) and update it with the associated value in that target element, as shown below:

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    firstName: "",
    lastName: "",
    age: "",
    username: "",
    password: "",
    email: "",
  });

  // Update specific input field
  const handleChange = (e) => 
    setUser(prevState => ({...prevState, [e.target.name]: e.target.value}))

  // Render UI
  return (
    <div className='App'>
      <form>
        <input type='text' onChange={handleChange} name='firstName' placeholder='First Name' />
        <input type='text' onChange={handleChange} name='lastName' placeholder='Last Name' />
        <input type='number' onChange={handleChange} name='age' placeholder='Age' />
        <input type='text' onChange={handleChange} name='username' placeholder='Username' />
        <input type='password' onChange={handleChange} name='password' placeholder='Password' />
        <input type='email' onChange={handleChange} name='email' placeholder='email' />
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

With this implementation, the event handler function is fired for each user input. In this event function, we have a setUser() state function that accepts the previous/current state of the user and unpacks this user state using the spread operator. Then we check the event object for whatever target element name that fired the function (which correlates to the property name in the state). Once this property name is gotten, we modify it to reflect the user input value in the form.

Conclusion

As a React developer creating highly interactive user interfaces, you have probably made some of the mistakes mentioned above. Hopefully, these helpful useState practices will help you avoid some of these potential mistakes while using the useState hook down the road while building your React-powered applications.

Writer: David Herbert


discord banner


Build your React-based CRUD applications without constraints

Building CRUD applications involves many repetitive task consuming your precious development time. If you are starting from scratch, you also have to implement custom solutions for critical parts of your application like authentication, authorization, state management and networking.

Check out refine, if you are interested in a headless framework with robust architecture and full of industry best practices for your next CRUD project.


refine blog logo

refine is a open-source React-based framework for building CRUD applications without constraints.
It can speed up your development time up to 3X without compromising freedom on styling, customization and project workflow.

refine is headless by design and it connects 30+ backend services out-of-the-box including custom REST and GraphQL API’s.

Visit refine GitHub repository for more information, demos, tutorials, and example projects.

Top comments (13)

Collapse
 
lukeshiru profile image
Luke Shiru

A few things:

For initial state

Instead of doing this:

import { useState } from "react";

const App = () => {
    const [user, setUser] = useState({
        image: "",
        name: "",
        bio: "",
    });

    return (
        <div className="App">
            <img src={user.image} alt="profile image" />
            <p>User: {user.name}</p>
            <p>About: {user.bio}</p>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

You could do this:

import { useState } from "react";

const App = () => {
    const [user, setUser] = useState();

    return (
        <div className="App">
            <img src={user?.image ?? ""} alt="profile image" />
            <p>User: {user?.name ?? ""}</p>
            <p>About: {user?.bio ?? ""}</p>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

That way you actually deal with the chance of those properties being missing instead of just having a solution for the initial render. You could also just do this:

import { useState } from "react";

const App = () => {
    const [user, setUser] = useState();

    return (
        <div className="App">
            {user !== undefined ? (
                <>
                    <img src={user.image} alt="profile image" />
                    <p>User: {user.name}</p>
                    <p>About: {user.bio}</p>
                </>
            ) : undefined}
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

So you only render whatever depends on user when user is set.

For optional chaining

Ideally you should only use it for stuff that could be undefined, in your example you go from user.names.firstName to user?.names?.firstName, but if names comes defined, then firstName should as well, so it should actually be: user?.names.firstName. TypeScript or JSDocs help a lot with this, if you have them configured correctly.

For Updating Specific Object Property

If you need to handle several properties, then ideally you should have several states. So instead of this:

import { useState } from "react";

export const App = () => {
    const [user, setUser] = useState({
        name: "John",
        age: 25,
    });

    return (
        <div className="App">
            <p>User: {user.name}</p>
            <p>Age: {user.age}</p>

            <button
                onClick={() =>
                    setUser(user => ({
                        ...user,
                        name: "Mark",
                    }))
                }
            >
                Change name
            </button>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

You should do this:

import { useState } from "react";

export const App = () => {
    const [userName, setUserName] = useState("Jhon");
    const [userAge, setUserAge] = useState(25);

    return (
        <div className="App">
            <p>User: {userName}</p>
            <p>Age: {userAge}</p>

            <button onClick={() => setUserName("Mark")}>Change name</button>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

For managing multiple input fields in forms

Ideally you shouldn't use state at all for that, and just use a form and the native FormData. Also your example sets onChange but doesn't set value, so you're mixing controlled with uncontrolled behavior, which is far from ideal.

Here's an example using FormData:

export const App = () => {
    const handleSubmit = event => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        // send formData to the back-end or use it how you please
    };

    return (
        <div className="App">
            <form onSubmit={handleSubmit}>
                <input type="text" name="firstName" placeholder="First Name" />
                <input type="text" name="lastName" placeholder="Last Name" />
                <input type="number" name="age" placeholder="Age" />
                <input type="text" name="username" placeholder="Username" />
                <input type="password" name="password" placeholder="Password" />
                <input type="email" name="email" placeholder="email" />
            </form>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
 
dikamilo profile image
dikamilo

I will add here: No not update state on component unmount. Very common mistake that also is in one of your examples:

import { useState } from "react";

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

  // Directly update state
  const update = () => setCount(count + 1);

  // Directly update state after 3 sec
  const asyncUpdate = () => {
    setTimeout(() => {
      setCount((currentCount) => currentCount + 1);
    }, 2000);
  };

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={update}>Add +1</button>
      <button onClick={asyncUpdate}>Add +1 later</button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

asyncUpdate method create timer that after 3 seconds, updates component state. After 3 seconds, the component may be unmounted (and timer still running in bacground since you didn't clean it), and we have a memory leak.

Two notes here:

  • always clear timeouts/intervals
  • check if component is mounted in your async code

const mounted = useRef(false);
let timeoutHandler= null;

useEffect(() => {
    mounted.current = true;

    return () => {
        mounted.current = false;

        // clear timeout on component unmount
        if(timeoutHandler) {
            clearTimeout(timeoutHandler)
        }
    };
}, []);

const asyncUpdate = () => {
    // still caveat here, multiple calls to asyncUpdate will overide timeoutHandler so we should clear it here befeore create new one
    if(timeoutHandler) {
        clearTimeout(timeoutHandler)
    }

    timeoutHandler = setTimeout(() => {
        // do not update component state when is not mounted
        if(mounted.current) {
            setCount((currentCount) => currentCount + 1);
        }
    }, 2000);
  };

Enter fullscreen mode Exit fullscreen mode
Collapse
 
kamil7x profile image
Kamil Trusiak

I think you posted wrong gif for async update. It shows updating to 6, instead of buggy update to 3.

Collapse
 
necatiozmen profile image
Necati Özmen Author

Thanks!. We'll check it

Collapse
 
efrenshou profile image
Efrén Vázquez Solís

Yes GIF is wrong, and I just spent a couple of minutes trying to figure out what was the bug because for me it was working right just to realize it's the wrong gif lol

Collapse
 
vishwastyagi profile image
Vishwas Tyagi

Luckily, I didn't run into these errors till now but now I am aware of the best practices to be followed while building React applications.
I found this guide helpful especially the "optional chaining" method. I didn't know it before.

Collapse
 
rafayeljamalyan profile image
rafayeljamalyan • Edited on

I'd rather title the article this way:

5 most common useState mistakes BEGINNER react developers often make

The current title may interest even more experienced react developers, to come and read the article, but indeed they'll just lose their time.

Don't get me wrong, the article is still great. You explain things very good, but that will be helpful only for beginners))

Collapse
 
omeraplak profile image
Ömer Faruk APLAK • Edited on

I think this is great news for you! congratulations 🚀

Collapse
 
kevincp17 profile image
kevincp17

As a soon to be fullstack software developer, I absolutely thankful for this information.

Collapse
 
necatiozmen profile image
Necati Özmen Author

Thanks Kevin:)

Collapse
 
julimuz profile image
Julian Muñoz

Wow, thanks for this advices, I’m begining with React, this is very useful

Collapse
 
anirseven profile image
Anirban Majumdar

Thank you for sharing this , someone who is starting with React this is really helpful. Cheers

Collapse
 
umamahesh_16 profile image
Umamaheswararao Meka

Very Good Article, "Updating useState Directly" is the one which I liked most as I never realized that React doesn't update the state immediately. Thank you for that !!!

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.