| name | flpbalada-react-use-state |
| description | Guides proper usage of React useState hook. Use this skill when adding state to components, deciding between useState vs alternatives, or troubleshooting state update issues. |
React: useState Hook Best Practices
Core Concept
useState is a React Hook that adds a state variable to your component, triggering re-renders when the state changes.
const [state, setState] = useState(initialState);
When to Use useState
Ideal Use Cases
| Use Case | Example |
|---|
| Form inputs | const [name, setName] = useState('') |
| UI state | const [isOpen, setIsOpen] = useState(false) |
| Simple counters | const [count, setCount] = useState(0) |
| Local component data | const [items, setItems] = useState([]) |
Use useState When
- State is local to the component
- State transitions are simple (direct value replacement)
- Changes should trigger re-renders
- You need to persist values between renders
When NOT to Use useState
Use useRef Instead
When you need mutable values that don't trigger re-renders:
const intervalRef = useRef(null);
const inputRef = useRef(null);
Use useReducer Instead
When state logic is complex:
const [state, dispatch] = useReducer(reducer, initialState);
Use useReducer when:
- State has multiple sub-values
- Next state depends on previous state in complex ways
- You want to centralize state logic
Avoid Redundant State
If a value can be computed from props or other state, don't store it:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
const fullName = `${firstName} ${lastName}`;
const sortedItems = useMemo(() =>
items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
Don't Use for Global/Shared State
For state shared across multiple components:
- React Context for moderate sharing
- External stores (Zustand, Jotai) for complex apps
- Server state libraries (TanStack Query) for async data
Critical Rules
1. Never Mutate State Directly
obj.x = 10;
setObj(obj);
setObj({ ...obj, x: 10 });
arr.push(item);
setArr(arr);
setArr([...arr, item]);
2. State Updates Are Asynchronous
function handleClick() {
setCount(count + 1);
console.log(count);
const nextCount = count + 1;
setCount(nextCount);
console.log(nextCount);
}
3. Use Updater Function for Sequential Updates
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
function handleClick() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}
4. Use Initializer Function for Expensive Initial Values
const [todos, setTodos] = useState(createTodos());
const [todos, setTodos] = useState(createTodos);
const [todos, setTodos] = useState(() => createTodos(userId));
5. Call Hooks at Top Level Only
if (condition) {
const [state, setState] = useState(0);
}
const [state, setState] = useState(0);
if (condition) {
}
Common Patterns
Resetting State with Key
<Form key={version} />
Storing Functions in State
const [fn, setFn] = useState(someFunction);
const [fn, setFn] = useState(() => someFunction);
setFn(() => newFunction);
Updating Objects/Arrays
setForm({ ...form, email: newEmail });
setUser({
...user,
address: { ...user.address, city: newCity }
});
setItems(items.filter(i => i.id !== id));
setItems([...items, newItem]);
setItems(items.map(i => i.id === id ? {...i, done: true} : i));
Quick Reference
DO
- Use for simple, local component state
- Create new objects/arrays when updating
- Use updater function when depending on previous state
- Use initializer function for expensive initial values
DON'T
- Store computed/derived values
- Mutate existing state objects/arrays
- Read state immediately after setting (it's a snapshot)
- Call
setState unconditionally during render
Alternative Hooks Comparison
| Hook | Use When |
|---|
useState | Simple state, primitives, basic objects |
useReducer | Complex state logic, multiple sub-values |
useRef | Mutable values without re-renders |
useMemo | Expensive computed values |
useContext | State shared across component tree |
References