React: 5 Small (Yet Easily Fixable) Mistakes Junior Frontend Developers Make With React State
Tags: react, webdev, javascript, beginners
Published: 2024-04-16 21:52:16

I have reviewed more than 1,000 front-end pull requests.

Like many junior developers, I made some common mistakes with React state when I started.

If you're in the same boat, here are 5 small mistakes you can quickly fix to use state properly in React:

## Mistake #1: Having an invalid state format

Your state should always be valid.

When we take a snapshot of your state, it should reflect a valid state of the world. This stops problems like:

- Having too verbose code
- Forgetting to reset some state properly
- Displaying the wrong UI under certain conditions

![Mistake 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17ue3z1st4pzbi09qfeu.gif)

----

**❌ Bad:** We shouldn't have `error,` `data,` and `isLoading` separate. Nothing prevents situations where `error` is present, `data` is present, and `isLoading` is still set to true.

```jsx
import { useState, useEffect } from "react";

export default function App() {
  const [error, setError] = useState();
  const [data, setData] = useState();
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // We have more work to do to update the state
    const fetchCatFacts = async () => {
      try {
        setIsLoading(true);
        setError(undefined);
        const response = await fetch("https://cat-fact.herokuapp.com/facts");
        const facts = await response.json();
        setData(facts);
      } catch (error) {
        setError(error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchCatFacts();
  }, []);

  return (
    <div className="App">
      <h1>Cat Facts</h1>
      {isLoading ? (
        <p>Loading…</p>
      ) : error != null ? (
        <p>Failed to load facts</p>
      ) : data != null ? (
        <ul>
          {data.map((d) => (
            <li key={d._id}>{d.text}</li>
          ))}
        </ul>
      ) : (
        <p>BAD: This should never happen but can if the state is not updated correctly </p>
      )}
    </div>
  );
}
```

**✅ Good:** We have a single state `data` that can only be in 4 states (neverLoaded, loading, error, loaded).
```jsx
export default function App() {
  const [data, setData] = useState({ type: "neverLoaded" });

  useEffect(() => {
    // The state is easier to update
    const fetchCatFacts = async () => {
      try {
        setData({ type: "loading" });
        const response = await fetch("https://cat-fact.herokuapp.com/facts");
        const facts = await response.json();
        setData({ type: "loaded", data: facts });
      } catch (error) {
        setData({ type: "error", error });
      }
    };

    fetchCatFacts();
  }, []);

  return (
    <div className="App">
      <h1>Cat Facts</h1>
      {(() => {
        // We can handle all states properly
        switch (data.type) {
          case "neverLoaded":
          case "loading":
            return <p>Loading…</p>;
          case "error":
            return <p>Failed to load facts</p>;
          case "loaded":
            return (
              <ul>
                {data.data.map((d) => (
                  <li key={d._id}>{d.text}</li>
                ))}
              </ul>
            );
        }
      })()}
    </div>
  );
}
```

## Mistake #2: Mutating the state vs. creating a new one

Never mutate state!

99% of the time, that's why you don't see your state changes happening.

Instead, make new objects/arrays every time so React knows the state is different and can update your app.

![Mistake 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z1rcwy87jgnibuiycnzw.gif)

----

**❌ Bad:** This code won't work because `todos` is being mutated instead of copied. So, React won't update because it sees the same object being used.
```jsx
import { useState } from "react";

export default function App() {
  const [inputValue, setInputValue] = useState("");
  const [todos, setTodos] = useState([]);

  const handleTodoAdd = () => {
    todos.push(inputValue);
    // This won't work since we are mutating `todos` and not creating a new object
    // So, from React perspective, nothing changed
    setTodos(todos);
  };

  return (
    <div className="App">
      <input
        value={inputValue}
        placeholder="Enter todo here"
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleTodoAdd}>Add</button>
      <ul>
        {todos.map((todo, idx) => (
          <li key={idx}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}
```

**✅ Good:** The code below will work since `newTodos != todos`.
```jsx
import { useState } from "react";

export default function App() {
  const [inputValue, setInputValue] = useState("");
  const [todos, setTodos] = useState([]);

  const handleTodoAdd = () => {
    // We create a completely new object
    const newTodos = [...todos];
    newTodos.push(inputValue);
    // This will now properly
    setTodos(newTodos);
  };

  return (
    <div className="App">
      <input
        value={inputValue}
        placeholder="Enter todo here"
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleTodoAdd}>Add</button>
      <ul>
        {todos.map((todo, idx) => (
          <li key={idx}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}
```

```jsx
// You can also use this shorthand version
const handleTodoAdd = () => {
    setTodos(currentTodos => [...currentTodos, inputValue]);
};
```


> Tip 💡: If you're confused about why `newTodos != todos`, check the difference between primitive types and reference types in JavaScript ([article](https://www.freecodecamp.org/news/primitive-vs-reference-data-types-in-javascript/)).

### Mistake #3: Having too much state

Keep your state minimal.

The more state you have:

- The harder the code is to debug since it can change for various reasons
- The harder it is to keep everything in sync because you must remember to update all the states properly
- The slower it might get because updating state can happen one after another, causing multiple updates

![Mistake 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkxgw9xha90cyf8zgnt0.gif)

----

**❌ Bad:** `name` shouldn't be in the state since it can be derived from `firstName` and `lastName.`
```jsx
export default function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLasName] = useState("");
  const [name, setName] = useState("");

  useEffect(() => {
    setName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  return (
    <form className="App">
      {name.trim() !== "" && <div>Hello {name}</div>}
      <div className="input">
        <label htmlFor="firstName">First name</label>{" "}
        <input
          id="firstName"
          value={firstName}
          onChange={(e) => setFirstName(e.target.value)}
        />
      </div>
      <div className="input">
        <label htmlFor="lastName">Last name</label>{" "}
        <input
          id="lastName"
          value={lastName}
          onChange={(e) => setLasName(e.target.value)}
        />
      </div>
    </form>
  );
}
```

**✅ Good:** We dropped the `name` state and just derived it. The code is faster, easier to understand, and more concise.
```jsx
export default function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLasName] = useState("");
  const name = `${firstName} ${lastName}`;

  return (
    <form className="App">
      {name.trim() !== "" && <div>Hello {name}</div>}
      <div className="input">
        <label htmlFor="firstName">First name</label>{" "}
        <input
          id="firstName"
          value={firstName}
          onChange={(e) => setFirstName(e.target.value)}
        />
      </div>
      <div className="input">
        <label htmlFor="lastName">Last name</label>{" "}
        <input
          id="lastName"
          value={lastName}
          onChange={(e) => setLasName(e.target.value)}
        />
      </div>
    </form>
  );
}
```

### Mistake #4: Not leveraging `useReducer` enough

`useReducer` is great for a couple of reasons:
- You get a single place (the reducer) to track all state changes.
- Once created, the `dispatch` object stays the same. This means components that don't use state won't need to update if they're just dispatching actions.
- It's easily expandable; you can add more actions, and so on.

![Mistake 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/631ug99iwemnrvy0u8hb.gif)

----

**❌ Bad:** With lots of logic already, adding methods like bulk complete and edits will only make the code messier.

```jsx
export default function App() {
  const [inputValue, setInputValue] = useState("");
  const [todos, setTodos] = useState([]);

  const handleTodoAdd = () => {
    setTodos((t) => [
      ...t,
      { id: Math.random(), todo: inputValue, completed: false },
    ]);
    setInputValue("");
  };

  const handleTodoRemove = (id) => {
    setTodos((t) => t.filter((t) => t.id !== id));
  };

  const handleTodoToggle = (id) => {
    setTodos((t) =>
      t.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );
  };

  const handleTodosClear = (id) => {
    setTodos([]);
  };

  return (
    <div className="App">
      <input
        value={inputValue}
        placeholder="Enter todo here"
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleTodoAdd}>Add</button>
      <ul>
        {todos.map(({ todo, id }, idx) => (
          <li key={idx}>
            <input
              type="checkbox"
              value={todo.completed}
              onClick={() => handleTodoToggle(id)}
            />
            {todo} <button onClick={() => handleTodoRemove(id)}>x</button>
          </li>
        ))}
      </ul>
      {todos.length > 0 && <button onClick={handleTodosClear}>Clear</button>}
    </div>
  );
}
```

**✅ Good:** We encapsulate all the logic inside the reducer

```jsx
import { useState, useReducer } from "react";

// All the state update logic is done inside the reducer
const todoAppReducer = (state, action) => {
  switch (action.type) {
    case "addTodo":
      return [...state, { id: Math.random(), todo: action.payload }];
    case "removeTodo":
      return state.filter((t) => t.id !== action.payload.id);
    case "toggleTodo":
      return state.map((t) =>
        t.id === action.payload.id ? { ...t, completed: !t.completed } : t
      );
    case "clearTodos":
      return [];
    default:
      return state;
  }
};

export default function App() {
  const [inputValue, setInputValue] = useState("");
  const [todos, dispatch] = useReducer(todoAppReducer, []);

  const handleTodoAdd = () => {
    dispatch({ type: "addTodo", payload: inputValue });
    setInputValue("");
  };

  const handleTodoRemove = (id) => {
    dispatch({ type: "removeTodo", payload: { id } });
  };

  const handleTodoToggle = (id) => {
    dispatch({ type: "toggleTodo", payload: { id } });
  };

  const handleTodosClear = (id) => {
    dispatch({ type: "clearTodos" });
  };

  return (
    <div className="App">
      <input
        value={inputValue}
        placeholder="Enter todo here"
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleTodoAdd}>Add</button>
      <ul>
        {todos.map(({ todo, id }, idx) => (
          <li key={idx}>
            <input
              type="checkbox"
              value={todo.completed}
              onClick={() => handleTodoToggle(id)}
            />
            {todo} <button onClick={() => handleTodoRemove(id)}>x</button>
          </li>
        ))}
      </ul>
      {todos.length > 0 && <button onClick={handleTodosClear}>Clear</button>}
    </div>
  );
}
```

### Mistake #5: Accessing the new state before it's ready

Hands up if you're a junior dev and haven't made this mistake 🖐️.

In React, state updates don't happen immediately. So, if you want to use the new state, you have to:

- Access it when you're setting it
- Or, wait until the state has been updated


![Mistake 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ai546h80yswl0sx7634v.gif)

----

**❌ Bad:** The code below won't work properly because when we log `counter,` it is still equal to the current value and not `counter + 1`.

```jsx
import { useState } from "react";

export default function App() {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    setCounter(counter + 1);
    // `counter` is equal to the current value and not the next one
    alert(`This will be the next counter value: ${counter}`);
  };

  return (
    <div className="App">
      <span className="counter">{counter}</span>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
```

**✅ Good:** We correctly log the next counter value.

```jsx
import { useState } from "react";

export default function App() {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    const nextCounter = counter + 1;
    setCounter(nextCounter);
    alert(`This will be the next counter value: ${nextCounter}`);
  };

  return (
    <div className="App">
      <span className="counter">{counter}</span>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
```
---

Thank you for reading this post 🙏.

Leave a comment 📩 to share a mistake you made with React state and how you overcame it.

And Don't forget to Drop a "💖🦄🔥".

If you like articles like this, join my **FREE** newsletter, **[FrontendJoy](https://frontendjoy.substack.com/)**.

If you want daily tips, find me on [X/Twitter](https://twitter.com/_ndeyefatoudiop).