React's useState hook is an essential tool for managing state in functional components, but it's easy to stumble into some common pitfalls. Whether you’re just starting out with React or have been working with it for a while, avoiding these mistakes can save you from unexpected bugs and performance issues.
Let’s walk through 10 frequent mistakes and how you can avoid them to write cleaner and more efficient code.
1. Wrong Initial State Type
One of the most common issues arises when the initial state type doesn’t match the type expected during state updates.
❌ Mistake: Initial State Type Mismatch
const [count, setCount] = useState(0);
setCount("1"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
✅ Solution: Use TypeScript or specify the type explicitly.
const [count, setCount] = useState<number>(0);
setCount(1); // No issues now.
2. Not Using Functional Updates
When updating state based on the previous value, referencing the current state directly can lead to stale values, especially in async operations.
❌ Mistake: Using Current State Directly
setCount(count + 1); // Can cause bugs in async scenarios.
✅ Solution: Use the functional form for safe updates.
setCount((prevCount) => prevCount + 1); // Ensures you always have the latest value.
3. Storing Derived State
Avoid storing values in state that can be derived from other state or props. This can lead to unnecessary re-renders and synchronization issues.
❌ Mistake: Storing Derived State
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(count * 2);
✅ Solution: Derive the value during render instead of using state.
const [count, setCount] = useState(0);
const doubleCount = count * 2; // No need to store this in state.
4. State Updates Inside the Render Phase
Calling setState inside the render phase is a recipe for infinite loops and performance issues.
❌ Mistake: Setting State During Render
const [count, setCount] = useState(0);
setCount(1); // Infinite loop!
✅ Solution: Trigger state changes in event handlers or effects.
const handleClick = () => setCount(1);
5. Directly Mutating State
React won’t detect changes if you mutate the state directly, especially with arrays or objects.
❌ Mistake: Mutating State Directly
const [items, setItems] = useState<number[]>([1, 2, 3]);
items.push(4); // Mutation happens here, React won’t re-render!
✅ Solution: Return a new array or object to trigger re-renders.
setItems((prevItems) => [...prevItems, 4]); // Spread to create a new array.
6. Undefined or Incorrect Types for Complex State
When dealing with complex state, not defining proper types can cause runtime issues and confusion.
❌ Mistake: Implicit Types Can Lead to Errors
const [user, setUser] = useState({ name: "", age: 0 });
setUser({ name: "John", age: "thirty" }); // Type error: Age should be a number.
✅ Solution: Define the shape of the state with correct types.
type User = { name: string; age: number };
const [user, setUser] = useState<User>({ name: "", age: 0 });
7. Using State for Mutable Values (Like Timers)
Using useState for values that don’t affect rendering, such as timers, leads to unnecessary re-renders.
❌ Mistake: Using State for Mutable Values
const [timerId, setTimerId] = useState<number | null>(null);
✅ Solution: Use useRef for mutable values that don’t need re-rendering.
const timerIdRef = useRef<number | null>(null);
8. Not Merging State Objects Properly
Unlike class components, useState does not merge updates automatically. Forgetting this can result in overwriting parts of your state.
❌ Mistake: Overwriting State Instead of Merging
const [user, setUser] = useState({ name: '', age: 0 });
setUser({ age: 25 }); // The 'name' field is now lost!
✅ Solution: Use the spread operator to merge state updates.
setUser((prevUser) => ({ ...prevUser, age: 25 })); // Merges with existing state.
9. Using State for High-Frequency Updates
Tracking high-frequency values like window dimensions in state can cause performance issues due to excessive re-renders.
❌ Mistake: Using State for Frequent Updates
const [size, setSize] = useState(window.innerWidth);
window.addEventListener("resize", () => setSize(window.innerWidth));
✅ Solution: Use useRef or debounce to reduce the performance hit.
const sizeRef = useRef(window.innerWidth);
useEffect(() => {
const handleResize = () => {
sizeRef.current = window.innerWidth;
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
10. Assuming State Updates Are Synchronous
React state updates are asynchronous, but many developers mistakenly assume that changes are applied immediately.
❌ Mistake: Assuming State Changes Are Immediate
setCount(count + 1);
console.log(count); // Logs the old value, not the updated one!
✅ Solution: Use useEffect to track state changes and ensure the latest value is used.
useEffect(() => {
console.log(count); // Logs the updated value after re-render.
}, [count]);
Final Thoughts 💡
Avoiding these useState pitfalls will make your React code more robust, readable, and performant. Understanding how React’s state mechanism works and knowing the best practices will save you time debugging and enhance your overall development experience.
Do you have any useState tips or mistakes to share? Drop them in the comments below! 👇
Top comments (0)