State Patterns and Best Practices
Now that you understand useState basics, let's explore common patterns, pitfalls, and best practices for managing state effectively.
Derived State (Computed Values)
Don't store values that can be calculated from existing state:
jsx1// BAD - redundant state2function Cart({ items }) {3 const [total, setTotal] = useState(0);45 useEffect(() => {6 setTotal(items.reduce((sum, item) => sum + item.price, 0));7 }, [items]);89 return <p>Total: ${total}</p>;10}1112// GOOD - derive from existing data13function Cart({ items }) {14 // Computed on every render - no extra state needed15 const total = items.reduce((sum, item) => sum + item.price, 0);1617 return <p>Total: ${total}</p>;18}
Rule: If you can calculate it from props or other state, don't add new state.
Common derived values:
- Totals, averages, counts
- Filtered/sorted lists
- Formatted display values
- Validation states (isValid = name.length > 0)
Normalizing State Structure
For complex data, consider normalizing (like a database):
jsx1// Nested structure (harder to update)2const [state, setState] = useState({3 users: [4 { id: 1, name: 'Alice', posts: [{ id: 1, title: 'Hello' }] }5 ]6});78// Normalized structure (easier to update)9const [state, setState] = useState({10 users: {11 1: { id: 1, name: 'Alice' }12 },13 posts: {14 1: { id: 1, userId: 1, title: 'Hello' }15 },16 userIds: [1],17 postIds: [1]18});1920// Update is simpler21const updatePost = (postId, title) => {22 setState(prev => ({23 ...prev,24 posts: {25 ...prev.posts,26 [postId]: { ...prev.posts[postId], title }27 }28 }));29};
Using Immer for Complex Updates
For deeply nested state, consider Immer:
jsx1import { produce } from 'immer';23function TodoApp() {4 const [state, setState] = useState({5 todos: [6 { id: 1, text: 'Learn React', done: false }7 ],8 filter: 'all'9 });1011 // Without Immer - verbose12 const toggleTodo = (id) => {13 setState(prev => ({14 ...prev,15 todos: prev.todos.map(todo =>16 todo.id === id ? { ...todo, done: !todo.done } : todo17 )18 }));19 };2021 // With Immer - feels like mutation but is immutable22 const toggleTodoImmer = (id) => {23 setState(produce(draft => {24 const todo = draft.todos.find(t => t.id === id);25 if (todo) todo.done = !todo.done;26 }));27 };28}
Immer lets you write "mutating" code that produces immutable updates.
State Colocation
Keep state as close as possible to where it's used:
jsx1// BAD - state at top level when only Form needs it2function App() {3 const [formData, setFormData] = useState({}); // Lifted too high45 return (6 <div>7 <Header />8 <Sidebar />9 <Form data={formData} onChange={setFormData} /> // Only user10 <Footer />11 </div>12 );13}1415// GOOD - state in the component that uses it16function App() {17 return (18 <div>19 <Header />20 <Sidebar />21 <Form /> {/* Manages its own state */}22 <Footer />23 </div>24 );25}2627function Form() {28 const [formData, setFormData] = useState({}); // Colocated29 // ...30}
When to lift state:
- Multiple components need the same state
- A parent needs to coordinate children
- Sibling components need to share data
Avoiding State Duplication
Don't duplicate data that exists elsewhere:
jsx1// BAD - duplicating items prop into state2function ItemList({ items }) {3 const [localItems, setLocalItems] = useState(items); // Duplicate!45 // Now items prop and localItems can get out of sync6}78// GOOD - use prop directly, lift changes to parent9function ItemList({ items, onItemsChange }) {10 // Use items directly, notify parent of changes11 const removeItem = (id) => {12 onItemsChange(items.filter(item => item.id !== id));13 };14}1516// GOOD - if you need local modifications, use key to reset17function ItemEditor({ item, key = item.id }) {18 const [draft, setDraft] = useState(item);1920 // When key changes (different item), state resets automatically21}
Resetting State
Three ways to reset component state:
1. Set state back to initial value
jsx1function Form() {2 const [name, setName] = useState('');34 const reset = () => setName('');56 return <input value={name} onChange={e => setName(e.target.value)} />;7}
2. Change the key prop (remount component)
jsx1function App() {2 const [formKey, setFormKey] = useState(0);34 const resetForm = () => setFormKey(k => k + 1);56 return <Form key={formKey} />; // New key = new component instance7}
3. Use useReducer with reset action
jsx1function reducer(state, action) {2 if (action.type === 'reset') return initialState;3 // ...other actions4}
Common State Pitfalls
1. Stale Closures
jsx1function Counter() {2 const [count, setCount] = useState(0);34 // Bug: always logs initial count (0) due to closure5 const handleClick = () => {6 setTimeout(() => {7 console.log(count); // Always 0!8 }, 3000);9 };1011 // Fix: use a ref or functional update12 const handleClickFixed = () => {13 setTimeout(() => {14 setCount(prev => {15 console.log(prev); // Current value16 return prev;17 });18 }, 3000);19 };20}
2. Object/Array Reference Equality
jsx1// Bug: infinite loop because {} !== {} on every render2useEffect(() => {3 fetchData(options);4}, [{ sort: 'name' }]); // New object every render!56// Fix: use primitive or memoize7const [sortBy, setSortBy] = useState('name');8useEffect(() => {9 fetchData({ sort: sortBy });10}, [sortBy]);
3. State Updates in Loops
jsx1// Bug: only increments by 12const incrementMany = () => {3 for (let i = 0; i < 5; i++) {4 setCount(count + 1); // All use same stale 'count'5 }6};78// Fix: use functional updates9const incrementMany = () => {10 for (let i = 0; i < 5; i++) {11 setCount(prev => prev + 1); // Each uses latest value12 }13};
4. Initializing State from Props (Anti-pattern)
jsx1// Anti-pattern: state initialized from props won't update2function UserCard({ user }) {3 const [name, setName] = useState(user.name);4 // If user prop changes, name state doesn't update!5}67// Better options:8// 1. Use prop directly (no state)9function UserCard({ user }) {10 return <h1>{user.name}</h1>;11}1213// 2. Use key to reset when prop changes14<UserCard key={user.id} user={user} />1516// 3. Controlled component17function UserCard({ name, onNameChange }) {18 return <input value={name} onChange={e => onNameChange(e.target.value)} />;19}
State Design Checklist
Before adding state, ask:
- Can it be derived? Calculate from props/other state instead
- Does it change? If not, use a constant or prop
- Who owns it? Keep state where it's used
- Is it duplicated? Use a single source of truth
- Is the structure right? Flat > nested, normalized > duplicated
useState vs useReducer
useReducer is better for:
- Complex state logic
- Multiple related values
- State transitions with clear actions
jsx1// useState - good for simple state2const [count, setCount] = useState(0);34// useReducer - good for complex state5const [state, dispatch] = useReducer(reducer, {6 items: [],7 loading: false,8 error: null9});1011// Actions are explicit12dispatch({ type: 'ADD_ITEM', item });13dispatch({ type: 'SET_LOADING', loading: true });14dispatch({ type: 'SET_ERROR', error });
We'll cover useReducer in depth in the Refs & Context topic.
Summary
State best practices:
- Derive when possible - Don't store computed values
- Colocate state - Keep it close to where it's used
- Avoid duplication - Single source of truth
- Use functional updates - When new state depends on old
- Watch for closures - Stale values in callbacks
- Reset with key - Change key to remount
- Consider useReducer - For complex state logic
Next, let's put these patterns into practice with a shopping cart workshop!