ホーム

Blog

Mastering useState in React — A Comprehensive Guide

useState is one of the most fundamental hooks in React. It gives function components the ability to hold and update state

更新日:2025/10/20

Mastering useState in React — A Comprehensive Guide

1 Introduction

useState is one of the most fundamental hooks in React. It gives function components the ability to hold and update state — a capability that used to require class components before hooks were introduced in React 16.8.

This guide aims to take you from a beginner level to advanced usage of useState. We'll cover simple examples, patterns for complex state, TypeScript usage, performance considerations, testing, and real-world examples.

Whether you're building small UIs or large applications, understanding how to use state well will improve your components' maintainability, performance, and correctness.

2 What is useState?

useState is a React Hook that lets you add state to function components. When you call it, React returns a pair: the current state and a function that updates it.

import React, { useState } from 'react';


function MyComponent() {
const [value, setValue] = useState(initialValue);
// ...
}
  • value is the current state value.
  • setValue is a function used to update the state.
  • initialValue is the initial state (can be any type).

    Calling setValue schedules an update and triggers a re-render.

    3 Installing and setting up a React project

If you want to follow along, create a new React app quickly with Vite or Create React App.

Using Vite (recommended for speed):

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

Using Create React App:

npx create-react-app my-app
cd my-app
npm start

Open src/App.jsx or src/App.tsx (TypeScript) and start coding.

4 Basic usage — a simple counter

Let's make a simple counter to illustrate useState.

import React, { useState } from 'react';


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


return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count   1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}

Every time the Increment button is clicked, setCount schedules an update and React re-renders the component with the new count.

Why useState returns an array

The hook returns an array (tuple) by design — it lets you name both the current value and the setter. You can use array destructuring to pick names that communicate intent.

5 Multiple state variables

Unlike class components where state is a single object, with hooks you can call useState multiple times to keep independent pieces of state.

function Form() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [subscribed, setSubscribed] = useState(false);


// ...
}

This approach improves readability and isolates updates; changing age won't affect name's setter or cause unnecessary merging.

6 State with complex objects and arrays

You can store objects and arrays in state, but remember that the setter replaces the state, it doesn't merge like setState in class components.

Example: updating an object

const [profile, setProfile] = useState({ name: 'Alice', age: 25 });


// To update only the name, copy the old object
setProfile(prev => ({ ...prev, name: 'Bob' }));

Example: updating an array

const [items, setItems] = useState([1, 2, 3]);


// Push a new item (immutable way)
setItems(prev => [...prev, 4]);


// Remove an item
setItems(prev => prev.filter(x => x !== 2));

Key point: use immutable patterns (spread operator, filter, map) so React can detect changes by reference.

7 Functional updates and the lazy initializer

7.1 Functional updates

When new state depends on the previous state, pass a function to the setter to avoid closure pitfalls.

setCount(prevCount => prevCount   1);

This ensures correct updates in concurrent scenarios and when multiple updates happen within the same render frame.

7.2 Lazy initial state

If computing the initial state is expensive, you can pass a function to useState so React computes it only once on mount.

const [heavy, setHeavy] = useState(() => expensiveComputation());

This is a useful optimization for heavy setups.

8 Event handling and state updates

Event handlers frequently update state. Keep these patterns in mind:

  • Prefer functional updates when the new state depends on the previous state.
  • Keep handler logic small; move complex logic to named functions if needed.
function Counter() {
const [count, setCount] = useState(0);


function handleClick() {
setCount(c => c   1);
}


return <button onClick={handleClick}>Increment</button>;
}

If you need to read the current state inside an async callback, note that state values might be stale due to closure. Use refs or functional updates to avoid stale closures.

9 State batching and asynchronous behavior

React may batch multiple state updates inside event handlers to reduce re-renders.

setA(1);
setB(2);
// React may re-render once with both updates

However, when updates happen inside promises, setTimeout, or other async callbacks, React (prior to automatic batching) might not batch them. Since React 18, automatic batching includes many asynchronous cases.

Because of this, when reading previous state, use the functional updater form to guarantee correctness.

10 Avoiding common pitfalls

10.1 Don't mutate state directly

// BAD — mutates array
items.push(4);
setItems(items);


// GOOD
setItems(prev => [...prev, 4]);

10.2 Don’t assume setState is synchronous

setCount(count   1);
console.log(count); // still old value in the same render

setCount schedules updates — read the new value in the next render or use updated value in setter callbacks.

10.3 Watch closures in event handlers

If an event handler closes over state, it keeps old values unless re-created. Solutions: include dependencies in useEffect or use functional updates.

11 Object vs multiple states — trade-offs

You can store multiple fields in a single object state or separate them:

11.1 Single object

const [state, setState] = useState({ a: 1, b: 2 });
setState(prev => ({ ...prev, a: 3 }));

11.2 Multiple states

const [a, setA] = useState(1);
const [b, setB] = useState(2);

Which to choose?

  • Use multiple states if fields are independent — smaller updates, fewer object copies.
  • Use one object when fields are tightly related or you want to pass them around as one unit.

Consider readability and frequency of updates when deciding.

12 Performance tips

  • Use functional updates when needed to avoid unnecessary re-renders or stale closures.
  • Keep state minimal — store only what you need to render. Derive other values.
  • For expensive derived values, use useMemo.
  • Avoid recreating handlers unnecessarily—use useCallback when passing handlers to memoized children.
  • In large lists, consider techniques like virtualization.

Example: avoid unnecessary state

// BAD: storing derived data
const [sorted, setSorted] = useState(sort(items));


// BETTER: compute on render or useMemo
const sorted = useMemo(() => sort(items), [items]);

13 useState with useEffect

useEffect runs side effects after render. Often you set local state based on props inside an effect.

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


useEffect(() => {
let mounted = true;
fetch(`/api/users/${userId}`).then(res => res.json()).then(data => {
if (mounted) setUser(data);
});
return () => { mounted = false; };
}, [userId]);
}

Watch out for race conditions — abort fetches or check mounted flags to avoid setting state on unmounted components.

14 Managing forms with useState

Simple forms map well to multiple useState calls.

function SignupForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');


function handleSubmit(e) {
e.preventDefault();
// validation, send to server
}


return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}

For larger forms, consider using useReducer, Formik, React Hook Form, or other form libraries.

15 Using useState with TypeScript

TypeScript can infer state types from the initial value, but sometimes you need to provide explicit generics.

import React, { useState } from 'react';


function Example() {
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<{ name: string } | null>(null);


return null;
}

If the initial value is null or undefined, explicitly provide the generic so TypeScript knows the intended type.

16 Patterns and architecture

16.1 Lifting state up

When two or more sibling components need the same data, lift the state up to the nearest common ancestor and pass state and setter via props.

16.2 Context for global-ish state

For truly global or cross-cutting state (theme, auth), use React Context. But keep it small — avoid putting frequently changing state in a context that triggers many re-renders.

16.3 When to use useReducer

useReducer is useful when state transitions are complex or when you want to centralize update logic (similar to Redux but local). It often simplifies updates for nested objects.