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 on SessionContext
  • CounterButtons depends on CounterContext
  • SessionCounterMessage depends on SessionContext 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>
  );
}

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=>
      <MyCustomComponent />
    </MyContext.Provider>
  );
}

is different from this:

const MyContext = React.useContext({});

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

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

function App() {
  return (
    <MyContextProvider>
      <MyCustomComponent />
    </MyContextProvider>
  );
}

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>
  );
}

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. ❤️


🏷️ Tags:

🗓️ Updated:




You may also enjoy

Buenas Practicas en Rails

¿Por qué son tan importantes las buenas prácticas en Ruby on Rails o en cualquier proyecto? Las razones son muchas pero 3 de las más importantes son: Mant...

Optimizing Active Record queries

Context: Some time ago, I worked on a project where I had to make a lot of reports. We had a lot of data, and most of the reports should be in the applicatio...

Authentication with Auth0 on Rails

One of the essential features that most applications have in common is Authentication. There are many options to authenticate us nowadays. In this article, I...

Optimización de queries en Rails

Contexto: Hace un tiempo, trabajé en un proyecto en el que tenía que dar reportes de la data que teníamos, la mayoría de los reportes que teníamos que calcul...