DEV Community

Kattya Cuevas
Kattya Cuevas

Posted on • Originally published at kattya.dev

Fixing Re-Renders When Using Context in React

Some months ago, I was refactoring a React project, and I was stuck in one problem for hours. The refactor was because of a common problem in React projects: Pass a lot of props to the child components, then you have to pass them to the child of them, and so. When this happens, if you want to reuse those components on another part of the app, you have to get information in your new component that maybe you don't need to worry about that time.

Versión en español aquí

I separated the data into many contexts, so I only share the necessary data with the component that needs them. So I stopped to pass a lot of props in every component. Even that sounds like a successful refactor, it wasn't. My components keep updating when I updated an state of a context which they didn't depend on. It doesn't make sense, right?

To explain my problem, I'll give you an example.
I'll have 3 components:

  • SessionForm: Component to add a username. If you have already entered it, it shows a greeting and a button to log out (delete the username). If you haven't entered it, it shows you an entry to add it.
  • SessionCounterMessage: Component that shows a message with the username entered or a You and the number returned by a counter.
  • CounterButtons: Component with a counter and 2 buttons that allow you to add or subtract from the counter.

Based on my first solution, I would create 2 contexts. One for the username (SessionContext) and one for the counter (CounterContext). Then the dependency of contexts of my components would look like this:

  • SessionForm depends onSessionContext
  • CounterButtons depends onCounterContext
  • SessionCounterMessage depends onSessionContext and CounterContext

This was my initial solution:

function App() {
  const [currentUser, setCurrentUser] = React.useState(null);
  const [counter, setCounter] = React.useState(1);

  return (
    <SessionContext.Provider
      value={React.useMemo(() => ({ currentUser, setCurrentUser }), [
        currentUser,
        setCurrentUser,
      ])}
    >
      <CounterContext.Provider
        value={React.useMemo(() => ({ counter, setCounter }), [
          counter,
          setCounter,
        ])}
      >
        <SessionForm />
        <SessionCounterMessage />
        <CounterButtons />
      </CounterContext.Provider>
    </SessionContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

I added a console.log to my components to make you aware of my error, I added a console.log to my components so that they see how many times it was rendered:

There you can see, when I update the counter, it re-renders the SessionForm component. Even when it doesn't depend on the CounterContext context, which has counter state.
And when I update the username, it re-renders the CounterButtons component. Even when it doesn't depend on the SessionContext context, which has username as a state.

Now you see my code, do you find my mistake?
Well, I didn't find any mistakes in my code if I had separated them into different contexts. Why did they keep re-render all the components?

What I did was ask for help. I asked @sergiodxa, who has been using React longer, and he said:
This

const MyContext = React.useContext({});

function App() {
  const [state, setState] = React.useState(false);

  return (
    <MyContext.Provider value={{ state, setState }}>
      <MyCustomComponent />
    </MyContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

is different from this:

const MyContext = React.useContext({});

function MyContextProvider({ children }) {
  const [state, setState] = React.useState(false);

  return (
    <MyContext.Provider value={{ state, setState }}>
      {children}
    </MyContext.Provider>
  );
}

function App() {
  return (
    <MyContextProvider>
      <MyCustomComponent />
    </MyContextProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

He didn't explain why at that time; maybe he was busy, I don't remember. But I realized that I was rendering my component in the same place that I created my states. Every time I updated the state, it re-rendered my parent component, which re-render all its children.

With this in my mind, I'll change my initial example to check it works.

function SessionProvider({ children }) {
  const [currentUser, setCurrentUser] = React.useState(null);

  return (
    <SessionContext.Provider
      value={React.useMemo(() => ({ currentUser, setCurrentUser }), [
        currentUser,
        setCurrentUser,
      ])}
    >
      {children}
    </SessionContext.Provider>
  );
}

function CounterProvider({ children }) {
  const [counter, setCounter] = React.useState(1);

  return (
    <CounterContext.Provider
      value={React.useMemo(() => ({ counter, setCounter }), [
        counter,
        setCounter,
      ])}
    >
      {children}
    </CounterContext.Provider>
  );
}

function App() {
  return (
    <SessionProvider>
      <CounterProvider>
        <SessionForm />
        <SessionCounterMessage />
        <CounterButtons />
      </CounterProvider>
    </SessionProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here you can see the logs when every component is rendered

It works! No more unnecessary renders!

It could look like a small change, and even you could think the user won't notice this change. But the components I was refactoring rendered audios and videos. Every time I updated the audios, the videos would be re-rendered, and it looks like a bug in the app.

If you made it this far, thanks for reading. ❤️

Oldest comments (0)