更新日:2025/10/20
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 — 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.
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);
// ...
}initialValue is the initial state (can be any type).
Calling setValue schedules an update and triggers a re-render.
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 devUsing Create React App:
npx create-react-app my-app
cd my-app
npm startOpen src/App.jsx or src/App.tsx (TypeScript) and start coding.
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.
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.
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.
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.
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.
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.
Event handlers frequently update state. Keep these patterns in mind:
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.
React may batch multiple state updates inside event handlers to reduce re-renders.
setA(1);
setB(2);
// React may re-render once with both updatesHowever, 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.
// BAD — mutates array
items.push(4);
setItems(items);
// GOOD
setItems(prev => [...prev, 4]);setCount(count 1);
console.log(count); // still old value in the same rendersetCount schedules updates — read the new value in the next render or use updated value in setter callbacks.
If an event handler closes over state, it keeps old values unless re-created. Solutions: include dependencies in useEffect or use functional updates.
You can store multiple fields in a single object state or separate them:
const [state, setState] = useState({ a: 1, b: 2 });
setState(prev => ({ ...prev, a: 3 }));const [a, setA] = useState(1);
const [b, setB] = useState(2);Which to choose?
Consider readability and frequency of updates when deciding.
// BAD: storing derived data
const [sorted, setSorted] = useState(sort(items));
// BETTER: compute on render or useMemo
const sorted = useMemo(() => sort(items), [items]);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.
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.
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.
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.
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.
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.
Mastering useState in React — A Comprehensive Guide
オフショア開発のご紹介資料