# Prevent Unnecessary Re-Renders of Components When Using useContext with React

When managing global state in React applications, `useContext` is a powerful and convenient tool. However, it comes with a common pitfall: **unnecessary re-renders** of components subscribing to the context. In this article,I'll explore why this happens, evaluate common solutions, and ultimately present a scalable approach using `useReducer` to solve this problem effectively.

I do prefer with showing an example that you can find in this [codesandbox](https://codesandbox.io/p/sandbox/blue-pine-vy5wmm?file=%2Fsrc%2FApp.js%3A16%2C1)

For example, in the code sandbox linked above, in the above situation all does work, and paragraph change cause console only when the button that listen to click is indeed clicked

But in App.js if you comment out `<AppContainerWithContext />` and load `<AppContainer />`, there are two states managed in the container, one called `titleText` and the other called `paragraphText`. `titleText` is passed as a prop to component called `TitleText` and `paragraphText` is passed to a component called `ParagraphText`. Both components are wrapped in `React.memo()`. There are two buttons called in the `AppContainer` and each has a function that changes the text back and forth based on the value of separate boolean states.

This happens when a state changes in context, every app (component) accessing data from that context re-renders, even if it's not utilizing the state data that changed (because it's all ultimately passed through the value object). React.Memo does not work on values accessed from context the same way it does for properties.

### **The Problem: Why Does useContext Trigger Unnecessary Re-Renders?**

React's `useContext` hook allows components to access shared state from a context provider. However, every time the context value changes, **all components consuming the context re-render**, even if the part of the context they rely on hasn't changed. This can lead to significant performance issues in large applications.

#### **Example of the Issue**:

Consider the following context and components:

```javascript
const AppContext = createContext();

export function AppProvider({ children }) {
  const [title, setTitle] = useState("Title A");
  const [paragraph, setParagraph] = useState("Paragraph A");

  const value = { title, setTitle, paragraph, setParagraph };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

export function useAppContext() {
  return useContext(AppContext);
}
```

Now, suppose you have two components, as in teh codesandbox before, `TitleText` and `ParagraphText`, each consuming a specific part of the context:

```javascript
const TitleText = () => {
  const { title } = useAppContext();
  return <h1>{title}</h1>;
};

const ParagraphText = () => {
  const { paragraph } = useAppContext();
  return <p>{paragraph}</p>;
};
```

When `setTitle` is called, both `TitleText` and `ParagraphText` will re-render because the entire context value object changes, even though only the `title` has been updated.

### **Common Solutions and Their Downsides**

One solution is indeed as illustrated with the initial code in the sandbox.

  
**1\. Using React.memo**

Wrapping components with `React.memo` can prevent unnecessary re-renders if the component’s props haven’t changed:

```javascript
export default React.memo(TitleText);
```

But this approach present 2 main downsides.

**Downsides**:

* **Dependency Management**: Ensuring props or context values passed to components are stable (e.g., wrapped with `useCallback` or `useMemo`) can quickly become cumbersome.
    
* **Inefficient for Large State**: For complex applications, it doesn’t scale well because every component still subscribes to the entire context.
    

Another approach is create a Context for each “main” state that is shared among component that need those state changes and based on those state change need to re-render.

This is called “Splitting Contexts” like:

```javascript
const AllContextProvider = props => {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotifProvider>
          <TimelineProvider>
            <CertProvider>
              <MenusProvider>
              {props.children}
              </MenusProvider>
            </CertProvider>
          </TimelineProvider>
        </NotifProvider>
      </ThemeProvider>
    </UserProvider>
  );
};
```

and the using as:

…

const TitleContext = createContext();

const ParagraphContext = createContext();

…

But this approach though is completely fine present some issue:

#### **2\. Splitting Contexts**

**Downsides**:

* Leads to "Provider Hell" where components are wrapped in multiple providers.
    
* Harder to manage and scale with more state variables.
    

### **A Scalable Solution: Refactoring to useReducer**

So a better approach is to refactoring from the initial `createContext` setup using `useState` to a `useReducer`\-based implementation provides a scalable and efficient solution I have wrote in this [codesandbox](https://codesandbox.io/p/sandbox/blue-pine-vy5wmm?file=%2Fsrc%2Fcontexts%2FAppContext.js%3A50%2C1).

This was considering the initial use of useState

#### **Initial Implementation with useState**

```javascript
import React, { createContext, useContext, useState } from "react";

const AppContext = createContext();

export function AppProvider({ children }) { const [title, setTitle] = useState("Title A"); const [paragraph, setParagraph] = useState("Paragraph A");

const value = { title, setTitle, paragraph, setParagraph };

return <AppContext.Provider value={value}>{children}</AppContext.Provider>; }

export function useAppContext() { return useContext(AppContext); }
```

While functional, this implementation suffers from re-render issues because `setTitle` and `setParagraph` cause the entire `value` object to change, leading to re-renders of all consuming components.

#### **Refactored Implementation with useReducer**

Using `useReducer` allows us to centralize state management and define explicit actions for state updates, reducing unnecessary re-renders. Here’s the refactored version:

```javascript
import React, { createContext, useContext, useReducer, useCallback } from "react";

// Initial state
const initialState = {
  title: "Title A",
  paragraph: "Paragraph A",
};

// Reducer function to handle state updates
const reducer = (state, action) => {
  switch (action.type) {
    case "SET_TITLE":
      return { ...state, title: action.payload };
    case "SET_PARAGRAPH":
      return { ...state, paragraph: action.payload };
    default:
      return state;
  }
};

// Create the context
const AppContext = createContext();

// Custom hook for consuming context with a selector
export function useAppContext(selector) {
  const { state } = useContext(AppContext);
  return selector ? selector(state) : state;
}

// Context provider
export function AppContextProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  const setTitle = useCallback((title) => {
    dispatch({ type: "SET_TITLE", payload: title });
  }, []);

  const setParagraph = useCallback((paragraph) => {
    dispatch({ type: "SET_PARAGRAPH", payload: paragraph });
  }, []);

  const value = {
    state,
    setTitle,
    setParagraph,
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
```

### **Selective Subscriptions in Components**

With `useReducer`, we can now implement selective subscriptions to ensure only the relevant parts of the state trigger re-renders:

#### **TitleText Component**

```javascript
import React from "react";
import { useAppContext } from "../contexts/AppContext";

const TitleText = () => {
  console.log("TitleText Triggered");

  // Subscribe to the title part of the state
  const title = useAppContext((state) => state.title);

  return <h1>{title}</h1>;
};

export default React.memo(TitleText);
```

#### **ParagraphText Component**

```javascript
import React from "react";
import { useAppContext } from "../contexts/AppContext";

const ParagraphText = () => {
  console.log("ParagraphText Triggered");

  // Subscribe to the paragraph part of the state
  const paragraph = useAppContext((state) => state.paragraph);

  return <p>{paragraph}</p>;
};

export default React.memo(ParagraphText);
```

This ensures `TitleText` re-renders only when the `title` changes, and `ParagraphText` re-renders only when the `paragraph` changes

### **Why This Works**

1. **Centralized State Management**:
    
    * The reducer handles all state updates in one place.
        
2. **Selective Subscriptions**:
    
    * Components only subscribe to the parts of the state they need, avoiding unnecessary re-renders.
        
3. **Scalability**:
    
    * Adding new state variables or actions is straightforward.
        
4. **Performance Optimization**:
    
    * This pattern minimizes re-renders while maintaining simplicity and readability.
        

### **Lessons from Redux and Meta**

This approach aligns with patterns established by Redux, which uses reducers and subscribable stores for efficient state management. React’s `useReducer` brings these concepts into its core API, offering a lightweight and performant solution without needing external libraries.

### **Conclusion**

When using `useContext`, unnecessary re-renders can be a significant challenge. While `React.memo` and splitting contexts offer partial solutions, adopting `useReducer` with selective subscriptions provides a scalable, maintainable, and efficient approach. This pattern combines the best of React’s core features with lessons learned from libraries like Met's Flux and Redux, making it ideal for managing global state in modern applications.
