If you've written a lot of React code, chances are you've used state incorrectly. One common mistake a lot of React developers do is storing states globally in the application, instead of storing them in the components where they're used.

Learn how you can refactor your code to utilize local state and why doing so is always a good idea.

Basic Example of State in React

Here's a very simple counter application that exemplifies how the state is typically handled in React:

        import {useState} from 'react'
import {Counter} from 'counter'

function App(){
  const [count, setCount] = useState(0)
  return <Counter count={count} setCount={setCount} />
}

export default App

On lines 1 and 2, you import the useState() hook for creating the state, and the Counter component. You define the count state and setCount method for updating the state. Then you pass both down to the Counter component.

The Counter component then renders the count and calls setCount to increment and decrement the count.

        function Counter({count, setCount}) {
  return (
    <div>
      <button onClick={() => setCount(prev => prev - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
    </div>
  )
}

You didn't define the count variable and setCount function locally inside the Counter component. Rather, you passed it in from the parent component (App). In other words, you're using a global state.

The Problem With Global States

The problem with using a global state is that you're storing the state in a parent component (or parent of a parent) and then passing it down as props to the component where that state is actually needed.

Sometimes this is fine when you have a state that is shared across lots of components. But in this case, no other component cares about the count state except for the Counter component. Therefore, it's better to move the state to the Counter component where it's actually used.

Moving the State to the Child Component

When you move the state to the Counter component, it'd look like this:

        import {useState} from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <button onClick={() => setCount(prev => prev - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
    </div>
  )
}

Then inside your App component, you don't have to pass anything down to the Counter component:

        // imports
function App(){
  return <Counter />
}

The counter will work exactly the same as it did before, but the big difference is that all of your states are locally inside this Counter component. So if you need to have another counter on the home page, then you'd have two independent counters. Each counter is self-contained and takes care of all of its own state.

Handling State in More Complex Applications

Another situation where you'd use a global state is with forms. The App component below passes the form data (email and password) and the setter method down to the LoginForm component.

        import { useState } from "react";
import { LoginForm } from "./LoginForm";

function App() {
  const [formData, setFormData] = useState({
     email: "",
     password: "",
  });

 function updateFormData(newData) {
    setFormData((prev) => {
      return { ...prev, ...newData };
    });
  }

 function onSubmit() {
    console.log(formData);
  }

 return (
    <LoginForm
      data={formData}
      updateData={updateFormData}
      onSubmit={onSubmit}
    />
  );
}

The LoginForm component takes in the login information and renders it. When you submit the form, it calls the updateData function which is also passed down from the parent component.

        function LoginForm({ onSubmit, data, updateData }) {
  function handleSubmit(e) {
    e.preventDefault();
    onSubmit();
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        type="email"
        id="email"
        value={data.email}
        onChange={(e) => updateData({ email: e.target.value })}
      />
      <label htmlFor="password">Password</label>
      <input
        type="password"
        id="password"
        value={data.password}
        onChange={(e) => updateData({ password: e.target.value })}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Rather than managing the state on the parent component, it's better to move the state to LoginForm.js, which is where you'll use the code. Doing so makes each component self-contained and not reliant on another component (i.e. the parent) for data. Here's the modified version of the LoginForm:

        import { useRef } from "react";

function LoginForm({ onSubmit }) {
  const emailRef = useRef();
  const passwordRef = useRef();
  
 function handleSubmit(e) {
    e.preventDefault();
    onSubmit({
      email: emailRef.current.value,
      password: passwordRef.current.value,
    });
  }

 return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input type="email" id="email" ref={emailRef} />
      <label htmlFor="password">Password</label>
      <input type="password" id="password" ref={passwordRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

Here you bind the input to a variable using ref attributes and the useRef React hook, rather than passing the update methods directly. This helps you remove verbose code and optimize the form performance using the useRef hook.

In the parent component (App.js), you can remove both the global state and updateFormData() method because you no longer need it. The only function left is onSubmit(), which you invoke from inside the LoginForm component to log the login details on the console.

        function App() {
  function onSubmit(formData) {
    console.log(formData);
  }

 return (
    <LoginForm
      data={formData}
      updateData={updateFormData}
      onSubmit={onSubmit}
    />
  );
}

Not only did you make your state as local as possible, but you actually removed the need for any state at all (and used refs instead). So your App component has gotten significantly simpler (having just one function).

Your LoginForm component also got simpler because you didn't need to worry about updating the state. Rather, you just keep track of two refs, and that's it.

Handling Shared State

There's one issue with the approach of trying to make the state as local as possible. You'd often run into scenarios where the parent component doesn't use the state, but it passes it to multiple components.

An example is having a TodoContainer parent component with two child components: TodoList and TodoCount.

        function TodoContainer() {
  const [todos, setTodos] = useState([])

  return (
    <>
      <TodoList todos={todos}>
      <TodoCount todos={todos}>
    </>
  )
}

Both of these child components require the todos state, so TodoContainer passes it to both of them. In scenarios like these, you have to make the state as local as possible. In the above example, putting it inside the TodosContainer is as local as you can get.

If you were to put this state in your App component, it would not be as local as possible because it's not the closest parent to the two components that need the data.

For large applications, managing the state just with the useState() hook can prove to be difficult. In such cases, you may need to opt for the React Context API or React Redux to effectively manage the state.

Learn More About React Hooks

Hooks form the foundation of React. By using hooks in React, you can avoid writing lengthy code that would otherwise use classes. The useState() hook is unarguably the most commonly used React hook, but there are many others such as useEffect(), useRef(), and useContext().

If you're looking to become proficient at developing applications with React, then you need to know how to use these hooks in your application.