DEV Community

Cover image for πŸš€ Day 14: Scaling Up with useReducer and Context API πŸš€
Mayuresh Surve
Mayuresh Surve

Posted on

πŸš€ Day 14: Scaling Up with useReducer and Context API πŸš€

Scaling Up with useReducer and Context in React

Welcome to Day 14 of my React Learning Journey! Over the past two days, I've dived deep into two powerful concepts in React: the useReducer hook and Context API. Today, I’ll be sharing how I combined these tools to refactor my application, scale it up for better state management, and what benefits this approach brings.

Recap: Where We Started

Previously, I was managing the state in my React application from Day 10 using the useState hook. While useState is straightforward and works well for simple state management, it starts to show its limitations as application grows. I had multiple stateful components and functions for adding, deleting, and toggling items in a list. As the logic became more complex, the code became harder to maintain and debug.

Here’s a quick look at what my code used to look like:

function App() {
    const [items, setItems] = useState([]);

    function handleAddItem(item) {
        setItems((items) => [...items, item]);
    }

    function handleDeleteItem(id) {
        setItems((items) => items.filter((item) => item.id !== id));
    }

    function handleToggleItem(id) {
        setItems((items) =>
            items.map((item) =>
                item.id === id ? { ...item, packed: !item.packed } : item
            )
        );
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

This approach worked fine initially but began to show drawbacks as the application grew. I realized it was time to scale up and make the code more robust and maintainable.

Enter useReducer and Context

To tackle the growing complexity, I decided to refactor my code using the useReducer hook and Context API. Here's how each of these concepts helped:

  1. useReducer Hook: This hook is perfect for managing more complex state logic. Unlike useState, which is ideal for simple state updates, useReducer centralizes the state management logic in one placeβ€”a reducer function. This makes the code more predictable, easier to debug, and better suited for handling complex state transitions.

  2. Context API: React’s Context API allows me to share state across multiple components without prop drilling. This means that instead of passing down state and functions as props through several layers, I can provide them via a context and access them wherever needed.

Refactoring the Application

Here’s what I changed in my application:

  1. Centralized State Management with useReducer:
    I replaced the useState hook with useReducer. Instead of having multiple functions handling state changes directly, I now have a single reducer function that takes care of all state transitions.

    import itemsReducer from "./ItemsReducer";
    
    function App() {
        const [items, dispatch] = useReducer(itemsReducer, []);
    
        // Rest of the code...
    }
    

    The reducer function (itemsReducer) now handles all state changes based on the action type:

    export default function itemsReducer(items, action) {
        switch (action.type) {
            case "add_item":
                return [...items, action.item];
            case "delete_item":
                return items.filter((item) => item.id !== action.id);
            case "toggle_item":
                return items.map((item) =>
                    item.id === action.id
                        ? { ...item, packed: !item.packed }
                        : item
                );
            case "clear_items":
                return [];
            default:
                throw new Error("Unknown action type: " + action.type);
        }
    }
    
  2. Using Context to Share State and Dispatch:
    I created two contexts: one for the state (ItemsContext) and one for the dispatch function (ItemsDispatchContext). This allows any component in the tree to access the items state or dispatch actions without prop drilling.

    //ItemsContext.js
    import { createContext } from "react";
    
    export const ItemsContext = createContext(null)
    export const ItemsDispatchContext = createContext(null)
    
    //App.js
    import { ItemsContext, ItemsDispatchContext } from "./itemsContext";
    
    function App() {
        const [items, dispatch] = useReducer(itemsReducer, []);
    
        return (
            <ItemsDispatchContext.Provider value={dispatch}>
                <AddItemForm />
                <ItemsContext.Provider value={items}>
                    <ItemsList />
                    <Footer />
                </ItemsContext.Provider>
            </ItemsDispatchContext.Provider>
        );
    }
    

    Now, components like AddItemForm, ItemsList, and Footer can simply use useContext to access the state and dispatch actions:

    import { useContext } from "react";
    import { ItemsDispatchContext } from "../itemsContext";
    
    function AddItemForm() {
        const dispatch = useContext(ItemsDispatchContext);
        // Dispatch actions directly
    }
    

Benefits of This Approach

  1. Centralized Logic: By using useReducer, all the state logic is centralized in one place (the reducer). This makes the state transitions more predictable and the code easier to test and debug.

  2. Decoupling State from UI: Using Context decouples the state management from the UI components. Components no longer need to pass down props for state management, making them cleaner and more focused on rendering UI.

  3. Scalability: As the application grows, adding more complex state transitions is easier with useReducer. The reducer can handle intricate logic without cluttering the component code.

  4. Ease of Maintenance: This refactoring makes the code more modular and easier to maintain. Future changes can be made in isolation without affecting unrelated parts of the code.

Answering Common Questions

  • Why not stick with useState?
    useState is great for simple cases, but as your app grows, managing state across multiple components with useState can become unwieldy. useReducer and Context provide a more scalable solution.

  • Isn’t useReducer overkill?
    Not really! For apps with complex state logic, useReducer simplifies and centralizes state transitions, making the codebase cleaner and more manageable.

  • How does Context help?
    Context eliminates the need for prop drilling, allowing state and dispatch functions to be shared across components efficiently. This results in cleaner and more modular components.

Conclusion

Refactoring my React application to use useReducer and Context has been a significant step forward. This approach not only made the code cleaner and more maintainable but also set the stage for scaling up the application with more complex state management needs.

I’m excited to continue this journey and explore more ways to improve my React applications. Stay tuned for more updates as I dive deeper into advanced React concepts!


Stay tuned for more updates, and feel free to follow my progress on GitHub and LinkedIn.

Cover Image by Ferenc Almasi on Unsplash

Top comments (0)