react-hook-usestate

The useState Hook in React

  • 5 min

So far, our components have been simple presenters. They receive data (Props) and render it. If the parent changes the props, the component would update.

But what if the change originates within the component? What if I want to type in an input, open a dropdown menu, or increment a counter on click?

This is where the concept of the component’s internal State comes in

State is the component’s internal memory. It’s the data that belongs to the component, changes over time, and when it changes, forces the interface to re-render.

To manage this in functional components, we use our first Hook: useState.

Hooks are special functions that allow your functional component (ephemeral) to “hook into” React features (persistent, like state or lifecycle).

By convention, all Hooks start with the prefix use (e.g., useState, useEffect, useContext).

The Problem with Local Variables

To understand why we need useState, let’s first “make a mistake” by trying it the “old way,” with regular JavaScript variables.

Look at this code, it seems logical, right?

export default function BrokenCounter() {
  let count = 0; // Local variable

  const increment = () => {
    count = count + 1;
    console.log(count); // In the console we'll see 1, 2, 3...
  };

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
    </div>
  );
}
Copied!

If you run this, you’ll see that

  • ✔️The console.log shows the variable going up
  • ❌ But the number on the screen (the <h1>) stays stuck at 0

For two fundamental reasons of React’s architecture:

  1. Lack of Reactivity: React has no way of knowing that count has changed. Modifying a local variable doesn’t notify React to trigger a re-render.
  2. Variable Scope: Even if we forced a re-render, when the BrokenCounter() function executes again, the line let count = 0 would run again, resetting everything.

The Solution: useState

To solve both problems, we have the useState Hook. It’s a function that provides us with a variable that:

  1. Persists between renders (React takes care of “remembering” it)
  2. Triggers a UI update when modified

To use it, first we import it from react,

import { useState } from 'react';
Copied!

And we use it like this:

const [state, setState] = useState(initialValue);
Copied!

What happens here is Array Destructuring. The useState function always returns an array with exactly two elements:

  1. The current value of the state (initially initialValue).
  2. A setter function to update that value.

The Counter That Actually Works

Let’s fix the previous example:

import { useState } from 'react';

export default function Counter() {
  // Declare a state variable called "count"
  const [count, setCount] = useState(0);

  const increment = () => {
    // Use the setter, do NOT modify the variable directly
    setCount(count + 1);
  };

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
    </div>
  );
}
Copied!

Now, when calling setCount(1):

React updates its internal memory: “count is now 1”.

React detects the change and re-executes (re-renders) the Counter function.

In this new execution, useState(0) no longer returns 0, but 1 (the remembered value).

The JSX uses this value and returns the HTML <h1>1</h1>.

React updates the DOM.

Usage Rules

  1. Never modify state directly: count = 5 will do nothing. You must use setCount(5).
  2. Hooks only at the top level: Never call useState inside loops, conditions (if), or nested functions. React relies on the order of Hook calls to know which state corresponds to which variable.

Not everything needs to be in state. Ask yourselves:

  • Is this data passed down via props from the parent? ➡ Not state.
  • Does it stay the same over time? ➡ Not state.
  • Can it be calculated from other state or props? ➡ Not state (it’s a derived variable).

Only use useState for data that changes over time and that you need to remember between renders to paint the interface.

Functional Updates

State updates in React can be asynchronous. If you try to update the state based on the previous value, doing this can be dangerous in fast scenarios:

// If we click very fast or inside loops
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
Copied!

It’s possible that, due to JavaScript’s closure, in all three lines count has the same value (e.g., 0), and the final result is 1 instead of 3.

To avoid this, when the new state depends on the previous one, we must pass a callback function to the setter:

// Correct and safe way (Functional Update)
setCount((previousValue) => previousValue + 1);
Copied!

Here React guarantees that previousValue is the most recent and real value it has in memory, just before applying the change.

State with Objects

The useState hook does not automatically merge objects. That is, if you have a complex object:

const [user, setUser] = useState({
  name: 'Luis',
  age: 30,
  email: '[email protected]'
});
Copied!

And you want to change only the email, you must manually copy the rest of the properties, or you will lose them.

// ❌ WRONG: This deletes name and age
setUser({ email: '[email protected]' });

// ✅ CORRECT: Use the Spread Operator to copy the previous state
setUser({ 
  ...user, // Copies name and age
  email: '[email protected]' // Overwrites email
});
Copied!

If the state has many unrelated properties, it’s often better to split them into multiple independent useState calls (const [name, setName], const [age, setAge]) instead of having one giant object.