useState - A Comprehensive Guide
The useState
hook is one of the most fundamental hooks in React’s functional components. It allows you to add state management to your functional components without converting them to class components.
Basic Syntax
const [state, setState] = useState(initialValue)
const [state, setState] = useState(initialValue)
initialValue
: The initial state valuestate
: The current state valuesetState
: A function to update the state value
Simple Example
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
)
}
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
)
}
Updating State Based on Previous State
When updating state that depends on the previous state value, always use the functional form of the setState function:
// Correct way
setCount(prevCount => prevCount + 1)
// May lead to issues in certain scenarios
setCount(count + 1)
// Correct way
setCount(prevCount => prevCount + 1)
// May lead to issues in certain scenarios
setCount(count + 1)
Request Tracker Example
import { useState } from 'react'
export default function RequestTracker() {
const [pending, setPending] = useState(0)
const [completed, setCompleted] = useState(0)
async function handleClick() {
setPending(pending + 1) // Could be problematic in async contexts
await delay(3000)
setPending(pending => pending - 1) // Correct way - uses latest state
setCompleted(completed + 1)
}
/* Important Note:
Using setPending(pending - 1) would be incorrect here
as it wouldn't access the latest state value after the async delay.
Always use the functional form when updates depend on previous state in async operations.
*/
return (
<>
<h3>
Pending: {pending}
</h3>
<h3>
Completed: {completed}
</h3>
<button onClick={handleClick}>
Buy
</button>
</>
)
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
import { useState } from 'react'
export default function RequestTracker() {
const [pending, setPending] = useState(0)
const [completed, setCompleted] = useState(0)
async function handleClick() {
setPending(pending + 1) // Could be problematic in async contexts
await delay(3000)
setPending(pending => pending - 1) // Correct way - uses latest state
setCompleted(completed + 1)
}
/* Important Note:
Using setPending(pending - 1) would be incorrect here
as it wouldn't access the latest state value after the async delay.
Always use the functional form when updates depend on previous state in async operations.
*/
return (
<>
<h3>
Pending: {pending}
</h3>
<h3>
Completed: {completed}
</h3>
<button onClick={handleClick}>
Buy
</button>
</>
)
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
Using Objects with useState
You can store objects in state:
const [user, setUser] = useState({ name: 'John', age: 25 })
// To update a property
setUser(prevUser => ({ ...prevUser, age: 26 }))
const [user, setUser] = useState({ name: 'John', age: 25 })
// To update a property
setUser(prevUser => ({ ...prevUser, age: 26 }))
Using Arrays with useState
const [items, setItems] = useState([])
// Add item
setItems(prevItems => [...prevItems, newItem])
// Remove item
setItems(prevItems => prevItems.filter(item => item.id !== itemId))
// Update item
setItems(prevItems => prevItems.map(item =>
item.id === itemId ? { ...item, name: newName } : item
))
const [items, setItems] = useState([])
// Add item
setItems(prevItems => [...prevItems, newItem])
// Remove item
setItems(prevItems => prevItems.filter(item => item.id !== itemId))
// Update item
setItems(prevItems => prevItems.map(item =>
item.id === itemId ? { ...item, name: newName } : item
))
Lazy Initialization
For expensive initial state calculations, use a function:
const [state, setState] = useState(() => {
const initialState = performExpensiveCalculation()
return initialState
})
const [state, setState] = useState(() => {
const initialState = performExpensiveCalculation()
return initialState
})
Multiple State Variables vs. Single Object
You can choose between multiple state variables:
const [name, setName] = useState('John')
const [age, setAge] = useState(25)
const [name, setName] = useState('John')
const [age, setAge] = useState(25)
Or a single object:
const [user, setUser] = useState({ name: 'John', age: 25 })
const [user, setUser] = useState({ name: 'John', age: 25 })
Choose based on what state variables change together and your specific component needs.
Common Pitfalls
Forgetting that setState is asynchronous:
setCount(count + 1) console.log(count) // May still show the old value
setCount(count + 1) console.log(count) // May still show the old value
Direct mutation of objects or arrays:
// Incorrect const newUser = user newUser.name = 'Jane' setUser(newUser) // This won't trigger a re-render // Correct setUser({ ...user, name: 'Jane' })
// Incorrect const newUser = user newUser.name = 'Jane' setUser(newUser) // This won't trigger a re-render // Correct setUser({ ...user, name: 'Jane' })
Not using the functional update form in event handlers with closures:
function handleClick() { setTimeout(() => { // Incorrect - may use stale state setCount(count + 1) // Correct - always uses latest state setCount(prevCount => prevCount + 1) }, 1000) }
function handleClick() { setTimeout(() => { // Incorrect - may use stale state setCount(count + 1) // Correct - always uses latest state setCount(prevCount => prevCount + 1) }, 1000) }
Best Practices
- Use multiple
useState
calls for unrelated state variables - Use a single
useState
with an object for related state variables - Always use the functional update form when new state depends on previous state
- Don’t call hooks conditionally or in loops
- Consider custom hooks for reusable stateful logic
The useState
hook, while simple in concept, provides powerful state management capabilities for React functional components.