DEV Community

loading...

Evitar Re-Renders al usar Context en React

Kattya Cuevas
Software Engineer who loves shared knowledge. Organizer RubyPeru & TechTalks. WTM Ambassador Lima
Originally published at kattya.dev ・3 min read

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)

Forem Open with the Forem app