Don't Get Hooked! Common React Mistakes and How to Fix Them

React's hooks are foundational for functional components, but they come with their own set of challenges. Even in 2024, junior developers often encounter pitfalls when using these hooks. Here are some common mistakes and how to avoid them:

  1. Initializing State with Functions that Perform Computations

     const expensiveCalculation = () => {
       // Some expensive computation
       return result;
     };
    
     // Mistake: Calling the function directly
     const [value, setValue] = useState(expensiveCalculation());
    
     // Correct: Using a function to initialize state
     const [value, setValue] = useState(() => expensiveCalculation());
    

    Calling the function directly results in the expensive calculation being executed on every render. Use a function to initialize state to ensure the calculation is performed only once.

  2. Not Batching State Updates

     const [count,setCount] = useState(0)
     // Mistake: Multiple state updates in sequence
     setCount(count + 1);
     setCount(count + 1);
    
     // Correct: Batching state updates using a callback
     setCount(prevCount => prevCount + 1);
     setCount(prevCount => prevCount + 1);
    

    Without using a callback, multiple state updates can result in incorrect state values. Always use the callback form to ensure state updates are based on the latest state.

  3. Updating object state

     const [user, setUser] = useState({ name: '', city: '', age: 0 });
     const handleChange = (e) => {
         // Incorrect approach:
         user.name = e.target.value;
         setUser(user);
         // Correct approach:
         setUser({
           ...user,
           name: e.target.value,
         });
       };
    

    When working with React state that involves objects, ensuring correct state updates is crucial for maintaining application correctness and performance. Always prefer immutable updates by creating new objects or arrays rather than mutating the existing state directly. This approach aligns with React's principles of state management and ensures predictable rendering and behavior of your components.

  4. Single object state instead of multiple smaller ones

     //Incorrect approach: 
     const [firstName,setFirstname] =  useState('')
     const [lastName,setLastname] =  useState('')
     const [email,setEmail] =  useState('')
     const [password,setPassword] =  useState('')
     const [address,setAddress] =  useState('')
     const [zipCode,setZipCode] =  useState('')
    
     //Correct approach:
     const [form, setForm] = useState({
         firstName: '',
         lastName: '',
         email: '',
         password: '',
         address: '',
         zipCode: '',
         // Add more fields as needed
       });
    

    In most cases, consolidating related state into a single state object offers clarity, simplicity, and maintainability benefits. It aligns with React's principles of managing state as a single source of truth, promoting cleaner code architecture and easier debugging.

  5. Information can be derived from state / props

     const PRICE_PER_ITEM = 5;
    
     const MyComponenet = () =>{
         const [quantity,setQuantity] = useState(0)
    
         //Incorrect approach
         const [totalPrice,setTotalPrice] = useState(0)
         useEffect(()=>{
             setTotalPrice(quantity*PRICE_PER_ITEM)
         },[quantity])
    
         //Correct approach
         const totalPrice = quantity * PRICE_PER_ITEM;
    
     }
    

    When developing with React, it's crucial to derive state from existing data whenever possible. This approach results in cleaner, more efficient, and maintainable code. By avoiding unnecessary state variables and effects, you simplify your components and streamline state management, reducing complexity and the risk of synchronization bugs. This principle applies broadly, ensuring your components remain focused and robust.

  6. Not Using Keys Correctly

     //Assuming item object has id property
    
     //Incorrect Approach:
     <ul>
       {items.map((item, index) => (
         <li key={index}>{item.name}</li>
       ))}
     </ul>
    
     //Correct Approach:
     <ul>
       {items.map(item => (
         <li key={item.id}>{item.name}</li>
       ))}
     </ul>
    

    keys are a fundamental part of building dynamic and efficient React applications

    • Optimize rendering performance by helping React efficiently update the virtual DOM.

    • Ensure correct and predictable rendering of list items, even when they change order or state.

    • Prevent bugs related to state management and component updates.

    • Aid in debugging by providing clearer component structure and better error messages.

    By using unique and stable keys, usually derived from the data (e.g., a unique identifier), developers can ensure their applications are both performant and reliable.

  7. Not Cleaning Up Effects

     useEffect(() => {
       const handleResize = () => {
         console.log(window.innerWidth);
       };
       window.addEventListener('resize', handleResize);
    
       // Mistake: Missing cleanup
     }, []);
    
     // Correct: Including cleanup
     useEffect(() => {
       const handleResize = () => {
         console.log(window.innerWidth);
       };
       window.addEventListener('resize', handleResize);
    
       return () => {
         window.removeEventListener('resize', handleResize);
       };
     }, []);
    

    Neglecting to clean up effects in React can lead to memory leaks and unexpected behavior. Always return a cleanup function from your useEffect hook when dealing with resources like event listeners, subscriptions, or timers. This practice ensures that resources are properly released and prevents potential performance issues and bugs in your application. By understanding and implementing effect cleanups, you can maintain a more efficient and reliable React application.

  8. State Duplication

     import React, { useState } from 'react';
    
     // Incorrect Approach: State Duplication in Child Component
     const IncorrectChildComponent = ({ item }) => {
         const [likes, setLikes] = useState(item.likes);
    
         const handleLike = () => {
             setLikes(likes + 1);
         };
    
         return (
             <div>
                 <h3>Incorrect Approach</h3>
                 <img src={item.image} alt="item" />
                 <p>Likes: {likes}</p>
                 <button onClick={handleLike}>Like</button>
             </div>
         );
     };
    
     // Correct Approach: Lifting State Up to Parent Component
     const CorrectChildComponent = ({ item, onLike }) => {
         return (
             <div>
                 <h3>Correct Approach</h3>
                 <img src={item.image} alt="item" />
                 <p>Likes: {item.likes}</p>
                 <button onClick={onLike}>Like</button>
             </div>
         );
     };
    
     const ParentComponent = () => {
         const [items, setItems] = useState([
             { id: 1, image: 'image1.jpg', likes: 0 },
             { id: 2, image: 'image2.jpg', likes: 0 },
             // More items...
         ]);
    
         const handleLike = (id) => {
             const updatedItems = items.map(item =>
                 item.id === id ? { ...item, likes: item.likes + 1 } : item
             );
             setItems(updatedItems);
         };
    
         return (
             <div>
                 <h2>Parent Component</h2>
                 {items.map(item => (
                     <div key={item.id}>
                         {/* Incorrect Approach Component */}
                         <IncorrectChildComponent item={item} />
    
                         {/* Correct Approach Component */}
                         <CorrectChildComponent 
                             item={item} 
                             onLike={() => handleLike(item.id)} 
                         />
                     </div>
                 ))}
             </div>
         );
     };
    

    State duplication occurs when the same piece of state is maintained in multiple places (e.g., both parent and child components). This can lead to inconsistencies because updates to the state in one component do not automatically reflect in the other component.

    In React, the recommended approach to avoid state duplication is to lift the state up to the nearest common ancestor that needs to access or modify that state. This way, there is a single source of truth, and the state remains consistent across the application.

    By lifting the state up to the parent component in your example, you ensure that the state is managed in one place, avoiding synchronization issues and keeping the state consistent.