useState - Managing Component State

The useState hook is the foundation of state management in functional components. It returns a state value and a function to update it, replacing the need for this.state and this.setState from class components.

Basic Usage

The simplest form of useState takes an initial value and returns an array with the current state and a setter function:

import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

Key Points:

  • Array Destructuring: useState returns an array, so we use destructuring to get the value and setter
  • Naming Convention: The setter is typically named set + capitalized state name
  • Initial Value: Can be any type - string, number, object, array, boolean, etc.

State Updates

Direct Updates

For simple values, you can update state directly:

function TextInput() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type something..."
/>
);
}

Functional Updates

When the new state depends on the previous state, use a function to ensure you’re working with the latest value:

function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// ❌ This might not work as expected with rapid clicks
// setCount(count + 1);
// ✅ This always works correctly
setCount(prevCount => prevCount + 1);
};
const incrementByFive = () => {
// This will only increment by 1, not 5!
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// ✅ This correctly increments by 5
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementByFive}>+5</button>
</div>
);
}

Working with Objects and Arrays

Object State

Remember that useState doesn’t merge objects automatically like this.setState did in class components:

function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateName = (newName) => {
// ❌ This will replace the entire object
// setUser({ name: newName });
// ✅ Spread the previous state to keep other properties
setUser(prevUser => ({
...prevUser,
name: newName
}));
};
const updateUser = (updates) => {
setUser(prevUser => ({
...prevUser,
...updates
}));
};
return (
<div>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateUser({ email: e.target.value })}
placeholder="Email"
/>
<input
type="number"
value={user.age}
onChange={(e) => updateUser({ age: parseInt(e.target.value) })}
placeholder="Age"
/>
<p>User: {JSON.stringify(user, null, 2)}</p>
</div>
);
}

Array State

Working with arrays requires creating new arrays rather than mutating existing ones:

function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
// ✅ Create a new array with the new todo
setTodos(prevTodos => [
...prevTodos,
{
id: Date.now(),
text: inputValue.trim(),
completed: false
}
]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
const deleteTodo = (id) => {
setTodos(prevTodos =>
prevTodos.filter(todo => todo.id !== id)
);
};
return (
<div>
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a todo..."
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}

Lazy Initial State

If your initial state is expensive to compute, you can pass a function to useState:

function ExpensiveComponent() {
// ❌ This runs on every render
const [data, setData] = useState(expensiveCalculation());
// ✅ This only runs once on initial render
const [data, setData] = useState(() => expensiveCalculation());
// ✅ Also works with more complex initialization
const [user, setUser] = useState(() => {
const savedUser = localStorage.getItem('user');
return savedUser ? JSON.parse(savedUser) : { name: '', email: '' };
});
return <div>{/* component JSX */}</div>;
}
function expensiveCalculation() {
console.log('Running expensive calculation...');
// Simulate expensive operation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return result;
}

Multiple State Variables

You can use multiple useState calls in a single component:

function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
await login(username, password);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
disabled={isLoading}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
}

Common Patterns

Toggle Boolean State

function ToggleExample() {
const [isVisible, setIsVisible] = useState(false);
// Simple toggle
const toggle = () => setIsVisible(!isVisible);
// Or using functional update
const toggleFunctional = () => setIsVisible(prev => !prev);
return (
<div>
<button onClick={toggle}>
{isVisible ? 'Hide' : 'Show'}
</button>
{isVisible && <p>Now you see me!</p>}
</div>
);
}

Form State Management

function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
// Reset form
setFormData({ name: '', email: '', message: '' });
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Send</button>
</form>
);
}

Best Practices

  1. Use multiple state variables for unrelated data instead of one large object
  2. Use functional updates when new state depends on previous state
  3. Initialize state with functions for expensive calculations
  4. Keep state flat when possible to avoid complex update logic
  5. Consider useReducer for complex state logic with multiple sub-values

Common Mistakes

Mutating State Directly

// ❌ Don't mutate state directly
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // This won't trigger a re-render!
// ✅ Create new arrays/objects
setItems(prev => [...prev, 4]);

Forgetting Functional Updates

// ❌ This might not work as expected
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
// ✅ Always reliable
const increment = () => setCount(prev => prev + 1);

Next Steps

Now that you understand useState, you’re ready to learn about useEffect for handling side effects and lifecycle events in functional components.

The combination of useState and useEffect covers most of what you need for basic React component logic!

Share Feedback