DEV Community

Cover image for Best Practices for useState and useEffect in React
Mariana Caldas for Web Dev Path

Posted on

Best Practices for useState and useEffect in React

This article cover image was created with the assistance of DALL·E 3

Welcome to the world of React! A place where creating dynamic web pages is fun, exciting, and quite confusing sometimes. You've probably heard of useState and useEffect hooks. These are like tools in a toolbox, each with a special purpose. Let's make sense of when and how to use them, especially focusing on avoiding common mix-ups.

Meet useState: your single component state manager

Think of useState as a notebook where you jot down important things that your web page needs to remember. It's perfect for keeping track of things like user input, or calculations based on that input.

When to use useState

  • Remembering values: Use useState for things your component needs to remember and change, like a user's name or a to-do list item.
  • Calculating on the fly: When you have a value that needs to be updated based on other things you’re remembering (like adding up prices in a shopping cart), useState is perfect. That’s called a derived state.

Initializing state from props

Another powerful aspect of useState is initializing state based on props. This approach is particularly useful when your component needs to start with a predefined value, which can then be updated based on user interactions or other changes within your component.

In the example below, the WelcomeMessage component receives initialMessage as a prop and uses it to set the initial state for message. This state can later be updated, for example, in response to user actions.

function WelcomeMessage({ initialMessage }) {
  const [message, setMessage] = useState(initialMessage);

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

Introducing useEffect: the side-effect handler

Now, let's talk about useEffect. It's like a helper who does tasks outside of what's on the screen, like fetching data from the internet, setting up subscriptions, or doing things after your component shows up on the page.

When to use useEffect

  • Fetching data: If you need to grab data from somewhere else (like a server), useEffect is your go-to.
  • Listening to events: Setting up things like timers or subscriptions is what useEffect excels at.
  • Cleaning up: useEffect can also clean up after itself, which is handy for things like removing event listeners.

Let's explore scenarios that demonstrate the strength of useEffect.

Example: fetching user data

Fetching data from an API is a classic use case for useEffect.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Asynchronous function to fetch user data
    async function fetchUserData() {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    }

    fetchUserData();
  }, [userId]);

  // JSX for displaying user information...
}
Enter fullscreen mode Exit fullscreen mode

In this case, useEffect is ideal because data fetching is asynchronous and doesn't directly involve rendering the UI. The effect runs after rendering, ensuring that fetching data doesn't block the initial rendering of the component.

Example: window resize listener

Listening to the browser's window resizing and adjusting the component is a great use of useEffect. This is a side effect because it involves interacting with the browser's API and needs to be set up and cleaned up properly, which useEffect handles elegantly.

function ResponsiveComponent() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWindowWidth(window.innerWidth);
    }

    window.addEventListener('resize', handleResize);

    // Cleanup to remove the event listener
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  // JSX that adapts to the window width...
}
Enter fullscreen mode Exit fullscreen mode

A common mistake: misusing useEffect for derived state

A common mix-up is using useEffect for tasks that useState can handle more efficiently, like deriving data directly from other state or props. Let's clarify this with an example.

Bad example: misusing useEffect for filtering a list

Imagine you have a list of fruits and you want to show only the ones that match what the user types in a search box. You might think about using useEffect like this:

import React, { useState, useEffect } from 'react';

const fruits = ["Apple", "Banana", "Cherry"];

function FruitList() {
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredFruits, setFilteredFruits] = useState(fruits);

  useEffect(() => {
    setFilteredFruits(fruits.filter(fruit => 
      fruit.toLowerCase().includes(searchTerm.toLowerCase())
    ));
  }, [searchTerm]);

  return (
    <div>
      <h1>Search Filter</h1>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <ul>
        {filteredFruits.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're using useEffect to update filteredFruits every time searchTerm changes. This approach, however, isn't ideal. Why?

  • Unnecessary Complexity: useEffect introduces an additional layer of complexity. It requires tracking and updating another state (filteredItems) which depends on searchTerm.
  • Risk of Errors: With useEffect, you run the risk of creating bugs or performance issues, especially if your effect interacts with other states or props in complex ways.
  • Delayed Update: Since effects run after render, there can be a slight delay in updating filteredItems, potentially leading to a brief mismatch in what the user sees.

Better way: using useState for derived state

A simpler and more effective approach is to calculate the filtered list directly using useState, like this:

import React, { useState } from 'react';

const fruits = ["Apple", "Banana", "Cherry"];

function FruitList() {
  const [searchTerm, setSearchTerm] = useState('');

  const filteredFruits = fruits.filter(fruit => 
    fruit.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <h1>Search Filter</h1>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <ul>
        {filteredFruits.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this approach, filteredItems is recalculated immediately whenever searchTerm changes. This ensures the filtered list is always in sync with the user's input, without the extra step and delay of useEffect. It's simpler, more efficient, and reduces the chance of errors related to state synchronization.


Other common pitfalls and best practices

Forgetting dependencies in useEffect: A typical mistake is not correctly specifying the dependency array, leading to unexpected behavior.

An example of that would be omitting [userId] in the dependency array of the UserProfile useEffect example above, which could prevent the component from updating when the user changes.

Incorrect state updates: In useState, ensure to use the functional update form when the new state depends on the previous one.

Example:

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

 // Correct usage
const increment = () => {
    setCount(prevCount => prevCount + 1); 
  };

//Incorrect usage
// const increment = () => {
//   setCount(count + 1); 
//  };


  // JSX for displaying and updating the count...
}

Enter fullscreen mode Exit fullscreen mode

Using the functional update form in useState, like setCount(prevCount => prevCount + 1), ensures that you always have the most recent state value, especially important in scenarios where the state might change rapidly or in quick succession, such as in fast user interactions.


Conclusion

In React, useState and useEffect serve distinct purposes. useState is your go-to for tracking and reacting to changes within your component, ideal for direct state management and calculations based on state. On the other hand, useEffect is perfect for handling external operations and side effects, like fetching data or interacting with browser APIs. Understanding when to use each will greatly improve the efficiency and reliability of your React components.

Top comments (1)

Collapse
 
kmsaifullah profile image
Khaled Md Saifullah

Thank you for sharing this valuable blog with the world.