DEV Community

Cover image for React - A Technique To Improve Performance If You Are Reading Data From Context
Jean
Jean

Posted on

1

React - A Technique To Improve Performance If You Are Reading Data From Context

Introduction

If you are dealing with state update logic, using Context-API is an initially good way to go and inject a state into a nested tree of components. However, there are cases when managing state based on Context can lead to unwanted re-renders in your components. This article has the intention to show you an approach to avoid unnecessary re-renders in your React components if you are managing a piece of UI state based on Context.

An Initial Example

Imagine you are reading data from a Todos context, that would look something like:

import { createContext, Dispatch, PropsWithChildren, useReducer } from "react";

export type TodoDTO = {
  description: string;
};

export type TodosState = Array<TodoDTO>;

export type TodosStatePayloadAction = { type: "add_todo"; payload: string }; // | other payloads format here ...;

export type TodosContextValue = {
  todos: TodoDTO[];
  dispatch: Dispatch<TodosStatePayloadAction>;
};

export const TodosContext = createContext<TodosContextValue>({
  todos: [],
  dispatch: () => null,
});

export function TodosProvider({ children }: PropsWithChildren) {
  const [state, dispatch] = useReducer(todosReducer, []);
  return (
    <TodosContext.Provider value={{ todos: state, dispatch }}>
      {children}
    </TodosContext.Provider>
  );
}

function todosReducer(
  state: TodosState,
  action: TodosStatePayloadAction
): TodosState {
  switch (action.type) {
    case "add_todo": {
      return [...state, { description: action.payload }];
    }
    //other operations below ...
    default: {
      return [...state];
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Problematic

However, the code above is totally fine if you have just a small tree of components, in the scenario where you have tons of components reading from the same Context, whenever the value state in to-dos reducer change, all the components reading from it will also re-render, even though those components who do not necessarily read the value in todos but only the dispatch function.
Take a look in the example:

export function SearchBar() {
  const { dispatch } = useContext(TodosContext);
  const textValue = useRef<HTMLInputElement | null>(null);

  function handleSubmit(e: ChangeEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!textValue.current?.value) return;

    dispatch({ type: "add_todo", payload: textValue.current.value });
    textValue.current.value = "";
  }
  return (
    <form onSubmit={handleSubmit}>
      <input ref={textValue} />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the example above, even though SearchBar is only reading the dispatch function from TodosContext, because the state todos and dispatch are attached together in the same Context, SearchBar will re-render again when a new to-do is added.
Now imagine this scenario where instead of a simple todo state and dispatch, you have some more complex logic to deal with, and many components are reading from it. It would cause many components to re-render again, even if they are not necessarily dependent on the state value from Context.

Removing The Unintended Behavior

To fix this behavior, we can take an approach where we split the TodosContext into two. One Context for state injection into the component tree and another Context for the dispatch/handlers.
Let's see an example:

export const TodosContextState = createContext<TodosState>([]);
export const TodosContextDispatch = createContext<
  Dispatch<TodosStatePayloadAction>
>(() => null);

export function TodosProvider({ children }: PropsWithChildren) {
  const [state, dispatch] = useReducer(todosReducer, []);
  return (
    <TodosContextState.Provider value={state}>
      <TodosContextDispatch.Provider value={dispatch}>
        {children}
      </TodosContextDispatch.Provider>
    </TodosContextState.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

In our new implementation, we split the previous Context in two, so we are injecting to-dos state via TodosContextState and the dispatch function via TodosContextDispatch, in the new implementation, when our SearchBar component submit a new to-do, only the components that are reading from TodosContextState will re-render and not all the components that are reading just a piece of data from it like SearchBar that only read the dispatch function.
We only need to do a final adjustment and update the Context in our SearchBar component from TodosContext to TodosContextDispatch and update our components that are only reading the state value from TodosContext to TodosContextState, so it would be:

export function SearchBar() {
  const dispatch = useContext(TodosContextDispatch);
  const textValue = useRef<HTMLInputElement | null>(null);

  function handleSubmit(e: ChangeEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!textValue.current?.value) return;

    dispatch({ type: "add_todo", payload: textValue.current.value });
    textValue.current.value = "";
  }
  return (
    <form onSubmit={handleSubmit}>
      <input ref={textValue} />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The example is this article was trivial just for the sake of understanding, but the main lesson that you should leave with is that:

  • Splitting a single Context into a state/dispatch version is a good way to prevent unintended re-renders if you have components that only read a single piece of it and are not concerned about the whole state structure.

Billboard image

Deploy and scale your apps on AWS and GCP with a world class developer experience

Coherence makes it easy to set up and maintain cloud infrastructure. Harness the extensibility, compliance and cost efficiency of the cloud.

Learn more

Top comments (1)

Collapse
 
rotcivdev profile image
Victor Salles

Awesome explanation for state management using pure React!

Heroku

This site is powered by Heroku

Heroku was created by developers, for developers. Get started today and find out why Heroku has been the platform of choice for brands like DEV for over a decade.

Sign Up

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay