React useState Hook
Introduction
In React, the useState
hook is one of the most fundamental hooks introduced in React 16.8. It allows you to add state management to functional components, which was previously only possible in class components. Before hooks, if you needed to maintain state in a component, you had to use a class component. Now, with the useState
hook, functional components can be just as powerful while being more concise and easier to understand.
The useState
hook enables you to:
- Store and update state values in functional components
- Trigger re-renders when state changes
- Maintain component-specific data between renders
Basic Syntax
The useState
hook has a simple syntax:
const [state, setState] = useState(initialValue);
Let's break this down:
initialValue
: The value you want the state to start withstate
: The current state valuesetState
: A function that updates the state value and triggers a re-render
The square brackets []
represent array destructuring, which allows us to give custom names to our state variable and its setter function.
Your First useState Example
Let's create a simple counter component:
import React, { useState } from 'react';
function Counter() {
// Declare a state variable named "count" with initial value of 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
When you click the button:
- The
setCount
function is called with the new value (count + 1
) - React updates the
count
state - The component re-renders, showing the updated count
Multiple State Variables
You can use useState
multiple times in a single component:
import React, { useState } from 'react';
function UserForm() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [isSubscribed, setIsSubscribed] = useState(false);
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Enter your age"
/>
<label>
<input
type="checkbox"
checked={isSubscribed}
onChange={(e) => setIsSubscribed(e.target.checked)}
/>
Subscribe to newsletter
</label>
<p>Preview: {name}, {age} years old, {isSubscribed ? 'subscribed' : 'not subscribed'}</p>
</div>
);
}
This approach keeps your state variables separate and focused on specific pieces of data.
Using Objects with useState
For related state values, you might want to use an object:
import React, { useState } from 'react';
function ProfileForm() {
const [profile, setProfile] = useState({
firstName: '',
lastName: '',
email: '',
bio: ''
});
const handleChange = (e) => {
// Important: Spread the previous state to maintain other field values
setProfile({
...profile,
[e.target.name]: e.target.value
});
};
return (
<form>
<input
type="text"
name="firstName"
value={profile.firstName}
onChange={handleChange}
placeholder="First Name"
/>
<input
type="text"
name="lastName"
value={profile.lastName}
onChange={handleChange}
placeholder="Last Name"
/>
<input
type="email"
name="email"
value={profile.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="bio"
value={profile.bio}
onChange={handleChange}
placeholder="Tell us about yourself"
/>
<div>
<h3>Profile Preview:</h3>
<p>Name: {profile.firstName} {profile.lastName}</p>
<p>Email: {profile.email}</p>
<p>Bio: {profile.bio}</p>
</div>
</form>
);
}
When updating an object or array with useState
, you must create a new object or array (using the spread operator ...
or other methods). React uses reference equality to determine if the state has changed and if it should re-render.
Using Previous State
When updating state based on the previous state, always use the functional form of the setter function:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// Wrong way (may lead to stale state issues)
const incrementWrong = () => {
setCount(count + 1);
setCount(count + 1); // This will not increment twice as expected
};
// Correct way using previous state
const incrementCorrect = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // This will increment twice
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementWrong}>Increment Wrong</button>
<button onClick={incrementCorrect}>Increment Correct</button>
</div>
);
}
The wrong approach doesn't work as expected because both setCount
calls reference the same value of count
from the render when they were called. The correct approach uses a callback that receives the most up-to-date state.
Lazy Initial State
If your initial state is expensive to compute, you can pass a function to useState
:
import React, { useState } from 'react';
function ExpensiveInitialState() {
// This function will only run once during the initial render
const [data, setData] = useState(() => {
console.log("Computing initial state...");
// Imagine this is an expensive calculation
const result = [];
for (let i = 0; i < 1000; i++) {
result.push({ id: i, value: Math.random() });
}
return result;
});
return (
<div>
<p>Data length: {data.length}</p>
<button onClick={() => setData([])}>Clear data</button>
<button onClick={() => setData(prev => [...prev, { id: prev.length, value: Math.random() }])}>
Add item
</button>
</div>
);
}
By passing a function to useState
, the expensive computation only runs during the initial render, not on every render.
Real-World Example: Todo List
Let's build a simple todo list application using useState
:
import React, { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim() === '') return;
setTodos([
...todos,
{
id: Date.now(),
text: input,
completed: false
}
]);
setInput('');
};
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h2>Todo List</h2>
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a new task"
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<span onClick={() => toggleTodo(todo.id)}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
<div>
<p>Total tasks: {todos.length}</p>
<p>Completed: {todos.filter(todo => todo.completed).length}</p>
</div>
</div>
);
}
This example demonstrates:
- Using
useState
with arrays - Adding items to state
- Updating specific items in an array
- Removing items from state
- Managing multiple state variables that interact with each other
useState Lifecycle
Let's visualize the lifecycle of useState
to better understand how it works:
Best Practices
-
Keep state minimal: Only store values in state that actually need to trigger re-renders when they change.
-
Use multiple
useState
calls for unrelated data: This makes your code more readable and maintainable. -
Use objects for related data: If multiple state variables always change together or represent related data, use an object.
-
Always use the functional update form when updating based on previous state: This ensures you're working with the most recent state.
-
Don't call hooks inside conditions, loops, or nested functions: Hooks must be called in the same order on every render.
// ❌ Wrong: useState in a condition
function BadComponent() {
if (someCondition) {
const [state, setState] = useState(initialValue); // This will break React's hooks
}
}
// ✅ Correct: useState at the top level
function GoodComponent() {
const [state, setState] = useState(initialValue);
if (someCondition) {
// Use state here
}
}
Common Pitfalls
Forgetting to Create New References
When updating objects or arrays, make sure to create new references:
// ❌ Wrong: Mutating state directly
const handleClick = () => {
user.name = "New Name"; // This modifies the existing object
setUser(user); // React won't detect this change!
};
// ✅ Correct: Creating a new object
const handleClick = () => {
setUser({ ...user, name: "New Name" }); // Creates a new object
};
Updating State Multiple Times in One Event
Remember that state updates are batched in React, and the state variable itself is not immediately updated after calling the setter function:
// This only increments once because both calls use the same value of count
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
// This correctly increments twice
const handleClick = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
Summary
The useState
hook is a fundamental building block for adding state to functional components in React. It provides:
- A way to declare state variables in functional components
- A mechanism to update state and trigger re-renders
- The ability to maintain values between renders
By using useState
effectively, you can build complex, interactive React applications with cleaner, more maintainable code than was possible with class components.
Practice Exercises
- Create a form with multiple fields that uses a single state object to manage all form data.
- Build a counter with increment, decrement, and reset buttons that uses the functional update form.
- Implement a todo list that allows adding, removing, editing, and marking todos as complete.
- Create a custom form input component that maintains its own state but also reports changes to a parent component.
Additional Resources
- React Hooks Documentation
- A Complete Guide to useEffect - by Dan Abramov
- Thinking in React Hooks - by Amelia Wattenberger
- When to useMemo and useCallback - by Kent C. Dodds
Happy coding with React Hooks!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)