The useState hook is one of the most fundamental features in modern React development. If you're building functional components and need to manage data that changes over time, useState is your go-to tool. In this guide, we'll explore everything you need to know about useState, from basic syntax to real-world patterns that will make you a more confident React developer.
Table of Contents
- What is useState?
- Why We Need useState
- Syntax and Basic Usage
- Initializing State
- Updating State Values
- State Update Functions
- Multiple State Variables
- Practical Examples
- Common Patterns and Mistakes
- FAQ
What is useState?
useState is a React Hook that lets you add state to functional components. Before Hooks were introduced in React 16.8, only class components could manage internal state. With useState, functional components gained the same capability, making them the standard approach for building React applications.
State represents data that can change over time—things like user input, toggle switches, counters, form submissions, or fetched data. When state changes, React automatically re-evaluates your component and updates the user interface to reflect the new information.
Think of state as the component's memory. Without it, your component would be "stateless"—it would always render the same output regardless of user interactions.
Why We Need useState
Consider a simple scenario: you have a button that needs to count how many times it's been clicked. Without state, clicking the button wouldn't do anything visible because there's no way to track or remember the click count.
// ❌ This won't work - no state tracking
function ClickCounter() {
let count = 0;
const handleClick = () => {
count++; // This updates a variable, but React doesn't know to re-render
};
return (
<div>
<p>Clicks: {count}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
When you click the button, count increases internally, but React has no idea this happened. The component never re-renders, so the UI stays frozen at "Clicks: 0". This is where useState comes in—it tells React to track changes and update the UI accordingly.
Syntax and Basic Usage
The basic syntax for useState is straightforward. You import it from React, call it inside your component, and destructure the result into a state variable and a setter function.
import { useState } from 'react';
Then, inside your component:
const [count, setCount] = useState(0);
This line does three things:
- Imports the hook from React
- Declares a state variable (
count) initialized to0 - Creates a setter function (
setCount) to update that state variable
When you call setCount with a new value, React schedules a re-render of your component with the updated state.
Understanding the Array Destructuring
useState returns an array with exactly two elements. Using array destructuring (the square brackets syntax) is a convention, but you could technically do this:
const stateArray = useState(0);
const count = stateArray[0]; // Current state value
const setCount = stateArray[1]; // Setter function
However, destructuring is cleaner and more readable:
const [count, setCount] = useState(0);
Naming Convention
By convention, you name the state variable whatever makes sense for your data, and the setter function is the state variable name with set prefix in camelCase:
const [count, setCount] = useState(0)const [name, setName] = useState('')const [isOpen, setIsOpen] = useState(false)const [user, setUser] = useState(null)
This makes code self-documenting and easy to understand at a glance.
Initializing State
The argument you pass to useState() is the initial state value. This value is used only on the component's first render; subsequent renders use the current state value instead.
Initializing with Different Types
You can initialize state with any JavaScript value type:
TypeScript Version
import { useState } from 'react';
type MyComponentProps = {};
export default function MyComponent() {
// Number
const [count, setCount] = useState<number>(0);
// String
const [name, setName] = useState<string>('');
// Boolean
const [isActive, setIsActive] = useState<boolean>(false);
// Array
const [items, setItems] = useState<string[]>([]);
// Object
const [user, setUser] = useState<{ name: string; age: number }>({
name: '',
age: 0,
});
// Null (common for optional data)
const [data, setData] = useState<null | { id: number }>(null);
return <div>Component with various state types</div>;
}
JavaScript Version
import { useState } from 'react';
export default function MyComponent() {
// Number
const [count, setCount] = useState(0);
// String
const [name, setName] = useState('');
// Boolean
const [isActive, setIsActive] = useState(false);
// Array
const [items, setItems] = useState([]);
// Object
const [user, setUser] = useState({ name: '', age: 0 });
// Null (common for optional data)
const [data, setData] = useState(null);
return <div>Component with various state types</div>;
}
Lazy Initialization
If calculating the initial state is expensive (like parsing JSON, running complex logic, or making an API call), you can pass a function instead of a value. React will only call this function on the initial render:
TypeScript Version
import { useState } from 'react';
function performComplexCalculation(): number {
// Simulate expensive operation
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result;
}
export default function MyComponent() {
// Expensive calculation happens only once
const [state, setState] = useState<number>(() => {
// This function is called only on the first render
const expensiveValue = performComplexCalculation();
return expensiveValue;
});
return <div>{state}</div>;
}
JavaScript Version
import { useState } from 'react';
function performComplexCalculation() {
// Simulate expensive operation
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result;
}
export default function MyComponent() {
// Expensive calculation happens only once
const [state, setState] = useState(() => {
// This function is called only on the first render
const expensiveValue = performComplexCalculation();
return expensiveValue;
});
return <div>{state}</div>;
}
This pattern is rarely needed for simple components, but it's valuable for performance optimization in certain scenarios.
Updating State Values
There are two ways to update state: passing a new value directly, or using an updater function. Understanding the difference is crucial for writing reliable React code.
Method 1: Direct Value Update
The simplest approach is to pass the new value directly to the setter function:
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(5); // Directly set to 5
};
This works fine when your new state doesn't depend on the previous state. However, when you're working with state that builds on itself (like incrementing a counter), it can cause bugs.
Method 2: Updater Function (Recommended)
When your new state depends on the previous state, always use an updater function:
const [count, setCount] = useState(0);
const handleIncrement = () => {
// Use updater function for state that depends on previous state
setCount(prevCount => prevCount + 1);
};
Pro Tip: Use the updater function approach whenever your new state is derived from the previous state. This ensures React uses the most recent state value, especially in scenarios where multiple state updates might be batched together.
Why This Matters: The Batching Issue
React batches state updates for performance. If you rely on the current state value in a direct update, you might get stale data:
TypeScript Version
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ❌ Potentially problematic
setCount(count + 1); // Uses current count value
setCount(count + 1); // Uses same count value (batched)
setCount(count + 1); // Uses same count value (batched)
// Result: count becomes 1, not 3
// React batches these updates and uses the same "count" value for all three
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click 3 times at once</button>
</div>
);
}
JavaScript Version
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ❌ Potentially problematic
setCount(count + 1); // Uses current count value
setCount(count + 1); // Uses same count value (batched)
setCount(count + 1); // Uses same count value (batched)
// Result: count becomes 1, not 3
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click 3 times at once</button>
</div>
);
}
Now with the updater function:
TypeScript Version
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ✅ Correct approach
setCount(prevCount => prevCount + 1); // First update: 0 → 1
setCount(prevCount => prevCount + 1); // Second update: 1 → 2
setCount(prevCount => prevCount + 1); // Third update: 2 → 3
// Result: count becomes 3 ✓
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click 3 times at once</button>
</div>
);
}
JavaScript Version
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ✅ Correct approach
setCount(prevCount => prevCount + 1); // First update: 0 → 1
setCount(prevCount => prevCount + 1); // Second update: 1 → 2
setCount(prevCount => prevCount + 1); // Third update: 2 → 3
// Result: count becomes 3 ✓
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click 3 times at once</button>
</div>
);
}
State Update Functions
The updater function receives the current state as its argument and must return the new state value. This is where the real power of useState becomes apparent.
Simple Updater Functions
For numbers and simple values:
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount * 2);
setCount(prevCount => prevCount - 5);
Updater Functions with Objects and Arrays
When dealing with objects or arrays, remember that you need to create a new object/array reference (React uses reference equality to detect changes):
TypeScript Version
import { useState } from 'react';
interface User {
name: string;
age: number;
email: string;
}
export default function UserProfile() {
const [user, setUser] = useState<User>({
name: 'John',
age: 30,
email: 'john@example.com'
});
// ❌ Don't mutate directly
const handleBadUpdate = () => {
setUser(prevUser => {
prevUser.name = 'Jane'; // Direct mutation won't trigger re-render
return prevUser;
});
};
// ✅ Create new object with spread operator
const handleGoodUpdate = () => {
setUser(prevUser => ({
...prevUser,
name: 'Jane',
}));
};
return <div>User: {user.name}</div>;
}
JavaScript Version
import { useState } from 'react';
export default function UserProfile() {
const [user, setUser] = useState({
name: 'John',
age: 30,
email: 'john@example.com'
});
// ❌ Don't mutate directly
const handleBadUpdate = () => {
setUser(prevUser => {
prevUser.name = 'Jane'; // Direct mutation won't trigger re-render
return prevUser;
});
};
// ✅ Create new object with spread operator
const handleGoodUpdate = () => {
setUser(prevUser => ({
...prevUser,
name: 'Jane',
}));
};
return <div>User: {user.name}</div>;
}
This is a critical concept: React detects state changes by comparing the new state reference with the previous reference. If you mutate the existing object, React doesn't see a change because the reference is the same.
Multiple State Variables
In real components, you'll often need to track multiple pieces of state. You can call useState multiple times:
TypeScript Version
import { useState, FormEvent } from 'react';
export default function RegistrationForm() {
const [firstName, setFirstName] = useState<string>('');
const [lastName, setLastName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`Registered: ${firstName} ${lastName} (${email})`);
} finally {
setIsLoading(false);
setFirstName('');
setLastName('');
setEmail('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First name"
/>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Registering...' : 'Register'}
</button>
</form>
);
}
JavaScript Version
import { useState } from 'react';
export default function RegistrationForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`Registered: ${firstName} ${lastName} (${email})`);
} finally {
setIsLoading(false);
setFirstName('');
setLastName('');
setEmail('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First name"
/>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Registering...' : 'Register'}
</button>
</form>
);
}
Note: While you can call
useStatemultiple times, consider consolidating related state into a single object if you have many state variables. However, for simple cases, multipleuseStatecalls is perfectly fine and often more readable.
Practical Examples
Let's look at real-world scenarios where useState shines:
Example 1: Toggle Component
A simple toggle is one of the most common useState use cases:
TypeScript Version
import { useState } from 'react';
interface ToggleProps {
label: string;
}
export default function Toggle({ label }: ToggleProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
return (
<div className="toggle-container">
<button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
>
{label} {isOpen ? '▼' : '▶'}
</button>
{isOpen && (
<div className="toggle-content">
<p>This content is now visible!</p>
</div>
)}
</div>
);
}
JavaScript Version
import { useState } from 'react';
export default function Toggle({ label }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="toggle-container">
<button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
>
{label} {isOpen ? '▼' : '▶'}
</button>
{isOpen && (
<div className="toggle-content">
<p>This content is now visible!</p>
</div>
)}
</div>
);
}
Example 2: Form Input Handler
Managing form inputs is where useState really demonstrates its value:
TypeScript Version
import { useState, FormEvent, ChangeEvent } from 'react';
interface FormData {
username: string;
password: string;
rememberMe: boolean;
}
export default function LoginForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
password: '',
rememberMe: false,
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prevData => ({
...prevData,
[name]: type === 'checkbox' ? checked : value,
}));
};
const validateForm = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.username) newErrors.username = true;
if (!formData.password) newErrors.password = true;
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (validateForm()) {
console.log('Form submitted:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
style={{ borderColor: errors.username ? 'red' : 'gray' }}
/>
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
style={{ borderColor: errors.password ? 'red' : 'gray' }}
/>
</div>
<label>
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleChange}
/>
Remember me
</label>
<button type="submit">Login</button>
</form>
);
}
JavaScript Version
import { useState } from 'react';
export default function LoginForm() {
const [formData, setFormData] = useState({
username: '',
password: '',
rememberMe: false,
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prevData => ({
...prevData,
[name]: type === 'checkbox' ? checked : value,
}));
};
const validateForm = () => {
const newErrors = {};
if (!formData.username) newErrors.username = true;
if (!formData.password) newErrors.password = true;
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form submitted:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
style={{ borderColor: errors.username ? 'red' : 'gray' }}
/>
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
style={{ borderColor: errors.password ? 'red' : 'gray' }}
/>
</div>
<label>
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleChange}
/>
Remember me
</label>
<button type="submit">Login</button>
</form>
);
}
Example 3: List Management
Managing a dynamic list with useState:
TypeScript Version
import { useState, FormEvent } from 'react';
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
export default function TodoList() {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [input, setInput] = useState<string>('');
const [nextId, setNextId] = useState<number>(1);
const handleAddTodo = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (input.trim()) {
setTodos(prevTodos => [
...prevTodos,
{ id: nextId, text: input, completed: false },
]);
setNextId(prevId => prevId + 1);
setInput('');
}
};
const handleToggleTodo = (id: number) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const handleRemoveTodo = (id: number) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
return (
<div>
<form onSubmit={handleAddTodo}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => handleRemoveTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
JavaScript Version
import { useState } from 'react';
export default function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const [nextId, setNextId] = useState(1);
const handleAddTodo = (e) => {
e.preventDefault();
if (input.trim()) {
setTodos(prevTodos => [
...prevTodos,
{ id: nextId, text: input, completed: false },
]);
setNextId(prevId => prevId + 1);
setInput('');
}
};
const handleToggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const handleRemoveTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
return (
<div>
<form onSubmit={handleAddTodo}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => handleRemoveTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Common Patterns and Mistakes
✅ Pattern: Functional Updates for Dependent State
Always use the updater function form when your new state depends on the previous state:
// Good
setCount(prevCount => prevCount + 1);
// Also fine when independent of previous state
setName('Jane');
❌ Mistake: Mutating State Directly
React doesn't detect mutations to the same object reference:
// ❌ DON'T DO THIS
const [user, setUser] = useState({ name: 'John', age: 30 });
const handleBadMutation = () => {
user.name = 'Jane'; // Mutation happens
setUser(user); // React doesn't see a change (same reference)
};
// ✅ DO THIS INSTEAD
const handleGoodUpdate = () => {
setUser(prevUser => ({
...prevUser,
name: 'Jane', // New object created
}));
};
❌ Mistake: Calling useState Conditionally
Hooks must be called at the top level of your component function, never inside conditions:
// ❌ DON'T DO THIS
function MyComponent() {
if (someCondition) {
const [state, setState] = useState(0); // ❌ Invalid!
}
}
// ✅ DO THIS INSTEAD
function MyComponent() {
const [state, setState] = useState(0); // Called at top level
// Then use state conditionally
if (someCondition) {
return <div>{state}</div>;
}
}
✅ Pattern: Combining Related State
If you have several related state values, consider combining them into a single object:
// Many separate useState calls
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
// Better: combine related state
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
});
✅ Pattern: Using useReducer for Complex State
When state logic becomes complex with multiple related updates, consider useReducer:
import { useReducer } from 'react';
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
// More suitable for complex state transitions
}
See the related articles for more on useReducer.
FAQ
Q: Can I use useState in class components?
A: No. Hooks, including useState, are designed specifically for functional components. Class components use this.state and this.setState() instead. However, if you're working with modern React code, you should be using functional components with Hooks.
Q: What happens if I pass the same value to setState?
A: React will skip the re-render because it detects that the state hasn't changed (using shallow comparison). However, with objects and arrays, React compares references, not contents. So if you pass a new object with the same properties, React will still re-render.
Q: Is it bad to have many useState calls in one component?
A: Not necessarily. React handles multiple useState calls efficiently. However, if you have 10+ state variables tracking closely related data, consider consolidating into an object or using useReducer for better organization and maintainability.
Q: Can I use useState in a loop?
A: No. useState must be called at the top level of your component function. Calling it in loops, conditions, or nested functions breaks the Rules of Hooks. Each call to useState must happen in the same order on every render.
Q: How do I track previous state value?
A: You'll need to use a useRef or useEffect hook to achieve this. See the related articles for patterns on tracking previous values.
Q: What's the difference between using an updater function and passing a value directly?
A: The updater function receives the current state and is guaranteed to use the most recent value, even if multiple updates are batched. Passing a value directly uses the state from the closure, which can be stale if multiple updates happen synchronously.
Q: Can I set state asynchronously?
A: State updates are batched and processed asynchronously by React. You cannot await setState. If you need to perform actions after state updates, use the useEffect hook, which runs after state changes.
Q: Should I store function or object references in state?
A: Generally, no. If you need to store a callback function, consider using useCallback. If you have an object that changes frequently, keep primitive values in state instead and compute derived values when needed.
Key Takeaways
The useState hook is fundamental to modern React development. Here are the essential points to remember:
useStatelets functional components manage state just like class components withthis.state- Always use the updater function form when your new state depends on previous state
- Create new objects and arrays, don't mutate existing ones
- Call
useStateat the top level of your component—never conditionally - React batches state updates for performance, so use updater functions to ensure correct values
- Multiple
useStatecalls are fine, though you can combine related state into objects - Let TypeScript infer return types - you don't need React.FC or explicit return type annotations in most cases
Next Steps
Now that you understand useState, explore these related topics to deepen your React knowledge:
- Master useEffect – Learn how to manage side effects alongside state
- Custom Hooks Patterns – Create reusable logic with Hooks
- Form Handling Best Practices – Advanced patterns for managing form state
- React Performance Optimization – Optimize re-renders and state updates
Last Updated: November 23, 2025
Questions or suggestions? Share your thoughts in the comments below. Happy coding!
Google AdSense Placeholder
CONTENT SLOT