React Basics #5: State and useState

8 min read

Last time you learned about components and props. But every component you built was static — once it was painted on the screen, it never changed its appearance again. Real apps need to refresh the screen when the user clicks a button, types something, or new data arrives. This time you’ll learn about state, the way a component handles changing data.

Why doesn’t a regular variable work? #

The first thing that comes to mind is, “Can’t I just change a variable’s value?” Let’s try it.

src/App.jsx
function App() {
  let count = 0;

  function handleClick() {
    count = count + 1;
    console.log('count:', count);
  }

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

export default App;

When you click the button, the console shows count: 1, count: 2, count: 3 — the value increases nicely. But the number on the screen stays at 0. Why?

When React renders, it runs the component function once and reflects the result on the screen. Changing a regular variable doesn’t tell React to redraw, so the screen never updates. On top of that, every time the component function is called again, let count = 0 runs from scratch, so the changed value doesn’t even survive into the next render.

To make React redraw the screen and keep the value alive across renders, you need a special storage called state.

The useState hook #

You create state by calling a function called useState. This function is one of React’s hooks.

Note
A hook is a special function that lets you use React features inside a function component. Their names all start with use (useState, useEffect, useContext, …). We’ll cover hooks in detail later in the series; for now, think of them as “special functions React provides.”

Let’s rebuild the counter using useState.

src/App.jsx
import { useState } from 'react';

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

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

export default App;

This time, when you click the button, the number on the screen actually increases.

A closer look at useState #

Let’s go through the code line by line.

import { useState } from 'react';

You import useState from the react package. You always need to import a hook before using it.

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

Calling useState(0) returns an array of length 2. JavaScript’s destructuring assignment receives both values at once.

  • The first one, count: the current state value. Initially it’s the initial value passed to useState (0).
  • The second one, setCount: the function that updates the state. You must call this for React to redraw the screen.

You can name them whatever you like, but by convention you follow the [value, setValue] pattern. For name, use [name, setName]; for isOpen, use [isOpen, setIsOpen].

setCount(count + 1);

When you call setCount with a new value, React (1) updates the state and (2) re-renders the component. On re-render, the component function runs from the top again, and this time useState(0) returns the freshly updated value (1).

What happens when state changes #

Keep this picture in your head — once you do, React code becomes much easier to read.

  1. The user clicks the button
  2. The handleClick function runs → setCount(1) is called
  3. React updates the state to 1 and re-renders the component
  4. The App function runs from the top again
  5. This time useState(0) returns [1, setCount]
  6. New JSX (<p>Current count: 1</p>) is built
  7. React compares it with the previous screen and updates only the changed parts in the real DOM

The key point is that the entire component function runs again whenever state changes. That’s why a regular variable like let count = 0 declared inside the component is reinitialized every time and doesn’t preserve its value. State is stored separately somewhere inside React, so it survives into the next render.

Various types of state #

A state value can be any JavaScript value, not just a number.

various state examples
const [name, setName] = useState('');                  // string
const [isOpen, setIsOpen] = useState(false);           // boolean
const [items, setItems] = useState([]);                // array
const [user, setUser] = useState({ name: '', age: 0 }); // object
const [selected, setSelected] = useState(null);       // null

Never modify state directly #

This is the most common mistake. The following code does not work.

wrong example
const [count, setCount] = useState(0);

function handleClick() {
  count = count + 1;  // direct modification — no
}
wrong example (array)
const [items, setItems] = useState(['apple']);

function addItem() {
  items.push('banana');  // mutating the array directly — no
  setItems(items);
}

You must always change state by calling the set function with a new value. For arrays and objects, the rule is to create a new array/new object and pass it in.

correct example (adding to an array)
const [items, setItems] = useState(['apple']);

function addItem() {
  setItems([...items, 'banana']);  // build a new array and pass it
}
correct example (updating part of an object)
const [user, setUser] = useState({ name: 'Alice', age: 30 });

function birthday() {
  setUser({ ...user, age: user.age + 1 });  // build a new object and pass it
}

The most common pattern is to spread the existing value with ... and overwrite only the parts you want to change.

Note
“Why do I have to make a new array/object?” When React decides whether state has changed, it checks whether the reference is different. items.push(...) mutates the same array’s contents but keeps the same reference, so React assumes nothing has changed and doesn’t re-render. You have to pass a new array/object so React notices “oh, this is a different value” and refreshes the screen.

Functional updates #

When you update a state value based on its previous value, you can pass a function to the set function.

src/App.jsx
function handleClick() {
  setCount(prev => prev + 1);
}

This is almost the same as setCount(count + 1), but because it safely receives the previous value, it works correctly when you need to call it several times in a row.

problematic pattern
function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

This code looks like it should increment the count by 3 per click, but in reality it only increments by 1. All three calls see the same count value, so they all attempt count + 1.

safer with functional updates
function handleClick() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
}

This way each call receives the result of the previous one, so the count increases by exactly 3. setCount(count + 1) is fine for everyday use, but the functional update — which clearly says “update based on the previous value” — is the safer default.

Multiple states #

You can call useState as many times as you want inside one component.

src/LoginForm.jsx
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  // ... input handling logic ...
}

It’s typical to keep values of different kinds in separate state variables. You could group them into a single object, but you’d have to spread it every time you update part of it, which makes the code more verbose. For simple values, separate state variables are easier.

Try it yourself #

Let’s build a counter component with +1, -1, and Reset buttons.

Create a new file src/Counter.jsx.

src/Counter.jsx
import { useState } from 'react';

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

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

export default Counter;

Use it from src/App.jsx.

src/App.jsx
import Counter from './Counter';

function App() {
  return (
    <>
      <h1>Counter demo</h1>
      <Counter />
      <Counter />
    </>
  );
}

export default App;

Here’s something interesting. You used <Counter /> twice, and each counter has its own count and works independently. Pressing +1 on one doesn’t change the other’s number. State is stored separately per component instance.

Wrapping up #

In this article you learned about state — the tool a component uses to handle changing data — and the useState hook. The key takeaways:

  • Regular variables can’t refresh the screen → use useState
  • The pattern is const [value, setValue] = useState(initial)
  • Don’t modify state directly; always change it through the set function
  • For arrays/objects, build a new value and pass it in ([...arr, x], { ...obj, k: v })
  • When updating based on the previous value, the functional update (setX(prev => ...)) is safer

So far you’ve used event handlers like onClick without looking at them closely. In the next article, “React Basics #6: Event Handling,” we’ll dig into how React handles events and how to pull information out of the event object.

X