Hace algunos meses, estaba haciendo un refactor en un proyecto de React y me quedé horas intentando resolver el problema. El refactor se debía a un problema típico de proyectos de React, pasar muchos props a los componentes hijos para así pasarlos a los hijos de estos y así. Cuando este tipo de situaciones pasa, al querer actualizar alguno de los componentes o quizá solo quieras reutilizarlos en alguna otra parte, te obliga a tener información en tu nuevo componente que no necesitas en ese momento.

English version here

En fin, al terminar el refactor, separé la información en varios contextos, para así solo compartir la data necesaria con los componentes que lo necesitaban. Aunque suena como un refactor exitoso, no lo era, mis componentes se seguían actualizando cuando actualizaba un estado de un contexto del que no dependían. No tiene sentido, ¿verdad?

Para explicar mi problema, pondré un ejemplo. Tengo 3 componentes:

  • SessionForm: Componente para agregar tu username. Si ya lo has ingresado, entonces te muestra un saludo y un botón para desloguearte (borrar el username). Si no lo has ingresado, te muestra un input para agregarlo.
  • SessionCounterMessage: Componente que muestra un mensaje con el username ingresado o un ‘You’ y el número que devuelva mi contador.
  • CounterButtons: Componente que tiene un contador. Son 2 botones que puedes sumar o restar al counter.

Siguiendo mi primera solución, aquí crearía 2 contextos. Uno para el username (SessionContext) y otro para el counter (CounterContext). Entonces la dependencia de contextos de mis componentes quedaría así:

  • SessionForm depende de SessionContext
  • CounterButtons depende de CounterContext
  • SessionCounterMessage depende de SessionContext y CounterContext

Esta fue mi solución inicial:

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

Para que se den cuenta de mi error, agregué un console.log a mis componentes para que vean cuántas veces se renderizaba:

Allí pueden ver que cuando actualizo el counter, se vuelve a renderizar el componente SessionForm, a pesar de que no depende del contexto CounterContext que es quien tiene a counter como estado. Y que cuando actualizo el username se vuelve a renderizar el componente CounterButtons, que no depende del contexto SessionContext, que tiene a username como estado.

Ahora que vieron mi código, ¿encontraron el error? Bueno, yo no encontraba fallas en mi lógica. Si los había separado en diferentes contextos. Entonces ¿por qué se seguían renderizando todos los componentes?

Lo que hice fue pedir ayuda. Le pregunté a @sergiodxa que tiene más tiempo usando React y me dijo: Esto

const MyContext = React.useContext({});

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

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

Es diferente a esto:

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

No me explicó el por qué en ese momento, quizá estaba ocupado, no lo recuerdo. Pero me di cuenta que estaba renderizando mi componente en el mismo lugar que estaba creando mis estados. Así que cada vez que actualizaba el estado, volvió a renderizar mi componente padre, que a su vez renderizaba a todos sus hijos.

Con esto en mente, voy a cambiar el ejemplo que les dí al inicio, para comprobar que realmente funciona.

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

Aquí pueden ver los logs de las veces que se renderiza cada componente ¡Funciona! ¡No más renders innecesarios!

Puede parecer una cambio muy pequeño, incluso se puede llegar a pensar que el usuario no se va a dar cuenta. Pero los componentes que estaba refactorizando renderizaban audios y videos. Cada vez que hacían un cambio respecto a los audios, los videos se volvían a renderizar y se sentía como un bug en la aplicación.

Si llegaron hasta aquí, gracias por leerme. ❤️


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