Mitos y Leyendas de React

Rate this content
Bookmark

Las charlas exploran varios patrones y conceptos engañosos en React, que parecen ser "conocimiento común", pero que muchos (si no la mayoría) de los desarrolladores suelen equivocarse o simplemente no son conscientes de ellos. Algunos ejemplos, cubiertos en la charla (y más):
* que "un componente de React se vuelve a renderizar cuando sus props cambian" (esto no es cierto)
* que envolver un componente en React.memo evitará su re-renderizado (no siempre es cierto)
* que el uso de Context causa re-renderizados y es malo para el rendimiento (no siempre es cierto, a veces Context puede reducir el número de re-renderizados en la aplicación)
* que crear un elemento como este `const A = <Child />` es cuando se activa el ciclo de vida de renderizado de Child (no es cierto)

22 min
06 Jun, 2023

Video Summary and Transcription

Esta charla discute mitos y conceptos erróneos en React que pueden afectar los re-renderizados. Cubre re-renderizados innecesarios y la idea equivocada de que las props desencadenan re-renderizados. La charla también explora el papel de la memorización y el contexto en la reducción de re-renderizados. Además, destaca la importancia de utilizar correctamente el atributo key para optimizar el renderizado. La charla concluye discutiendo la separación de estado y API en el contexto y el uso de herramientas de gestión de estado como Redux.

Available in English

1. Introducción a los mitos y leyendas en React

Short description:

Bienvenidos a la charla sobre mitos y leyendas en React. Mi nombre es Nadia y he sido desarrolladora por mucho tiempo. Hoy quiero compartir algunos mitos y conceptos erróneos que pueden afectar negativamente las re-renders en todas las aplicaciones. Pero antes, recordemos rápidamente qué es el montaje, las re-renders y las re-renders innecesarias en React.

Trabajé en Atlassian durante aproximadamente cinco años en un producto que algunos de ustedes pueden conocer y amar llamado Jira. Hasta hace muy poco, vivía en Australia rodeada de loros y canguros. Pero hace unos meses, me cansé de este clima agradable y playas perfectas y me mudé a Austria por la única razón de que soy perezosa y Austria es más fácil de escribir.

También soy un poco nerd. Uno de mis pasatiempos nerds es investigar cómo funcionan las cosas en detalle y luego escribir artículos de deep dive sobre esos temas. Normalmente escribo para desarrolladores front-end, más específicamente para desarrolladores de React. Uno de los principales enfoques de esas investigaciones, lo que más me interesa últimamente es el rendimiento de React. Y el tema del rendimiento en React es increíblemente interesante. React es una herramienta fantástica que nos permite escribir aplicaciones complicadas de manera fácil y rápida. Pero como resultado, también es muy fácil escribir código que haga que nuestras aplicaciones sean muy lentas y tengan retrasos. Y la mayoría de las veces, esto se debe a las re-renders. Ya sea que volvamos a renderizar demasiado rápido, demasiado a menudo o componentes demasiado pesados. Por lo tanto, la clave para un buen rendimiento en React es saber y poder controlar cuándo se montan y se vuelven a renderizar los componentes. Y luego evitar los que son innecesarios.

Así que hoy quiero compartir algunos mitos y conceptos erróneos que son bastante comunes entre los desarrolladores y que pueden afectar negativamente la situación de las re-renders en todas las aplicaciones. Pero antes de hacer eso, recordemos rápidamente qué es el montaje, las re-renders y qué son las re-renders innecesarias. Todo comienza con el montaje. Esta es la primera vez que el componente aparece en la pantalla. En este momento, React inicializa y conecta todo, por lo tanto, la renderización inicial dispara todas las devoluciones de llamada y los efectos de uso por primera vez. Después de eso, si su aplicación es interactiva, llegará el momento de las re-renders. La re-renderización es cuando React actualiza un componente que ya existe con algunos nuevos datos. Esto suele ocurrir como resultado de la interacción del usuario con su interfaz o algunos datos externos que llegan. Las re-renders son una parte crucial del ciclo de vida de React. Sin ellas, no habrá actualizaciones en la interfaz y, como resultado, no habrá interactividad. Por lo tanto, no es algo de lo que queramos deshacernos.

2. Unnecessary Re-renders and the Big Re-renders Myth

Short description:

Las re-renders innecesarias ocurren cuando toda la aplicación se vuelve a renderizar en cada pulsación de tecla, lo que afecta negativamente al rendimiento. El mito de las grandes re-renders afirma que un componente se vuelve a renderizar cuando cambian sus props, pero esto no es cierto. Las re-renders se desencadenan por cambios en el estado, que propagan las actualizaciones a otros componentes de forma recursiva.

Las re-renders innecesarias, sin embargo, son una historia completamente diferente. Imagina una aplicación React, solo un árbol de componentes como cualquier otra aplicación, y en algún lugar en la parte inferior de este árbol, tenemos un componente con un campo de entrada donde el usuario puede escribir algo. Cuando esto sucede, quiero actualizar el estado de esta entrada y todo lo relacionado con los datos del usuario, como mostrar algunas sugerencias útiles mientras el usuario está escribiendo. Esto es una re-renderización. Lo último que quiero, sin embargo, es que toda la aplicación se vuelva a renderizar en cada pulsación de tecla. Imagina lo lento que sería escribir en este campo si algo así sucediera. Definitivamente, esto no es algo que alguien llamaría una aplicación de rendimiento. Este es un ejemplo de re-renders innecesarias, y es exactamente el tipo de re-renders que queremos eliminar.

Y cuando se trata de prevenir las re-renders, hay un gran mito en el que todos creen. Lo llamo el mito de las grandes re-renders. Va así. Un componente se vuelve a renderizar cuando cambian sus props. Es increíble, de verdad. Todos lo creen. Nadie lo duda. Y simplemente no es cierto. Para entender eso, profundicemos un poco más en por qué ocurren las re-renders en primer lugar. Todo comienza con el cambio de estado. Cualquier desarrollador de React probablemente reconocerá el código. Tenemos un componente padre. Renderiza un componente hijo y tiene un hook useState. Cuando se activa setState, el componente padre completo se volverá a renderizar. El cambio de estado es la fuente inicial de todas las re-renders. Es el rey de las re-renders, por así decirlo. Por eso está en la corona. Después del cambio de estado, es el momento de que React propague esta actualización a otros componentes. React lo hace de forma recursiva. Toma los hijos directos de un componente con estado, los vuelve a renderizar, luego vuelve a renderizar los hijos, y así sucesivamente hasta que llega al final del árbol de componentes o se detiene explícitamente. Esta es la siguiente razón por la que un componente se vuelve a renderizar cuando su componente padre se vuelve a renderizar. Si observamos el código, una vez que ocurre el cambio de estado en el componente padre, todos los hijos que tiene este componente se volverán a renderizar como resultado.

3. Re-renders and the Myth of Memoization

Short description:

Las actualizaciones de estado desencadenan re-renders en el componente padre, lo que a su vez provoca re-renders en todos sus hijos y sus hijos. React no compara props durante el ciclo de vida normal, por lo que memoizar props no primitivas es innecesario. La memoización del propio componente es crucial para que la memoización de props funcione de manera efectiva. El mito que rodea el contexto y los re-renders es controvertido, ya que el contexto puede ser una herramienta útil para reducir los re-renders al pasar datos a través de un gran árbol de componentes.

Se vería algo así. Se desencadena una actualización de estado. El componente padre se vuelve a renderizar. Los hijos se vuelven a renderizar, los hijos de los hijos se vuelven a renderizar, y así sucesivamente.

Una cosa interesante aquí es que, como puedes ver, no he mencionado nada sobre props aquí, ni una sola vez. ¿Qué sucederá si el componente hijo tiene algunas props? En realidad, nada. Durante el ciclo de vida normal de React, React en realidad no compara props en absoluto. Si hay un hijo, se volverá a renderizar.

Esto me lleva a otro mito que se deriva del primero, el mito de que debemos memoizar todas las props no primitivas en un componente para evitar que se vuelva a renderizar. Nuevamente, esto es un mito, no es cierto. Como resultado de esta creencia, es bastante común ver código que se ve así. Un callback se envuelve en useCallback, lo cual está bien si es solo uno, pero luego habrá otro y otro hasta que la lógica hermosa del componente se entierre bajo este lío incomprensible e ilegible de useMemos y useCallbacks. Sin ninguna razón en absoluto. Todos esos son inútiles y no previenen nada. Todo esto porque olvidamos un paso importante aquí. La memoización del propio componente. Para que la memoización de props funcione como esperamos, el componente también debe ser memoizado. Debe envolverse en un React.memo. Solo entonces React se detendrá antes de volver a renderizar y comprobará sus props. Finalmente, este es el caso en el que la prop onChange debe envolverse en useCallback. Solo si ninguna de las props cambia, React se detendrá y no volverá a renderizar más.

El siguiente mito, probablemente el más controvertido. Contexto. Desafortunadamente, el contexto tiene mala reputación cuando se trata de re-renders. Algunas veces, por supuesto, se lo merece. Pero también debido a esta reputación, a veces las personas no se dan cuenta de que el contexto puede ser una herramienta muy útil en la lucha contra los re-renders. Veamos por qué. Imagina nuevamente un gran árbol de componentes con un componente en algún lugar en la parte superior que desea pasar algunos datos al componente en la parte inferior, el componente rosa. Y simplemente pasamos estos datos a través de props como de costumbre. ¿Qué sucederá desde la perspectiva de los re-renders? Nada bueno. El estado cambia en la parte superior, todos los hijos se vuelven a renderizar, sus hijos de los hijos también se vuelven a renderizar hasta que estos datos lleguen al componente que realmente los necesita.

4. Contexto y Re-renders

Short description:

El contexto nos permite evitar el árbol de componentes y evitar re-renders innecesarios. Podemos extraer datos y usarlos directamente en los componentes, reduciendo los re-renders. Al utilizar el contexto, podemos simplificar estructuras de componentes complejas y reducir el código. Sin embargo, el contexto puede causar re-renders en todos los componentes hijos, incluso si no utilizan los datos. Dividir los proveedores de contexto en valores separados puede ayudar a mitigar este problema.

El contexto, sin embargo, nos permite hacer trampa un poco y evitar ese árbol de componentes. Si simplemente extraemos estos datos que queremos pasar al contexto y los usamos directamente en el componente rosa, solo el componente con los propios datos y el componente que realmente los utiliza se volverán a renderizar. El resto de la aplicación simplemente se quedará allí en silencio y no hará absolutamente nada.

De acuerdo, los gatitos son divertidos, pero echemos un vistazo más de cerca a la vida normal y a los componentes normales. Digamos que quiero implementar una página que se vea así. Diseño de dos páginas con navegación a la izquierda, área de contenido principal a la derecha. En la navegación quiero tener un botón que expanda y colapse la navegación. Y quiero renderizar un número diferente de bloques en la parte inferior del área de contenido dependiendo de si la navegación está expandida o colapsada. Si comienzo a implementarlo con props normales, se vería algo así. Tendríamos una página que mantiene el estado de la navegación, pasa ese estado al botón que lo expande a través del componente de navegación y escucha la devolución de llamada en él. Y luego tendríamos que pasar estos datos al componente de la página a través de todas las capas, hasta el nivel donde necesito renderizar esos bloques. Y nuevamente, desde la perspectiva de los re-renders, no es genial. Cada expansión o colapso de la navegación resultará en re-renders de absolutamente todo.

Lo que puedo hacer en su lugar es extraer esta lógica de expansión y colapso en el contexto, solo la lógica, nada más. Esto se vería algo así, el mismo estado que controla la navegación, tendríamos un valor al que pasaríamos el propio estado y la función de alternancia, y luego pasaríamos este valor al proveedor de contexto. De repente, nuestra página, en lugar de toneladas de código y props por todas partes, se convierte en algo tan limpio como esto. Contexto en la parte superior, navegación y contenido pasado como hijos. Y luego, en algún lugar de la navegación, tendríamos el botón que simplemente usa la función de alternancia directamente desde el contexto. Y en algún lugar del área de contenido, tendríamos el bloque con los elementos que usan el estado de la navegación directamente desde el contexto.

Pero aunque este ejemplo parece genial en teoría, en la vida real, por supuesto, es un poco más complicado, como siempre. En la vida real, todavía tenemos el problema de que el contexto tiene mala reputación en cuanto a los re-renders. Principalmente, esta reputación proviene de dos hechos. El primero es que los re-renders relacionados con el contexto se desencadenarán en cada hijo que use este gancho de contexto, independientemente de si utiliza los datos reales o no. Entonces, en nuestro caso, el componente de la izquierda que solo usa la función de alternancia, en teoría, no debería preocuparse por el estado en absoluto, pero en la práctica, ambos componentes se volverán a renderizar cuando cambie el contexto. Y el segundo problema aquí es que estos re-renders son inevitables. No hay una forma sensata de evitarlos. Los ganchos de memoización o de devolución de llamada no ayudarán. Sin embargo, hay otro truco que puede ayudar aquí si este comportamiento realmente causa problemas de rendimiento. Podemos dividir esos proveedores de contexto en dos. En lugar de un solo valor que contenga tanto la alternancia como el estado, podemos tener dos valores separados.

5. Separando Estado y API en Contexto

Short description:

Al separar el estado expandido y el volumen de la API, el componente que utiliza el interruptor no se volverá a renderizar. El uso de contextos separados para los datos de estado y las funciones de la API garantiza que los componentes que utilizan la API no se vuelvan a renderizar. El contexto puede no ser necesario en todas las aplicaciones, ya que se pueden utilizar herramientas de gestión de estado como Redux. Comprender el contexto facilita la gestión del estado.

Y ahora, cuando los separamos, cuando el estado expandido ha cambiado, el volumen de la API, el objeto que contiene la función de alternancia, no cambiará. Por lo tanto, el componente que utiliza la alternancia no se volverá a renderizar. En el código, se vería algo así.

En lugar de este gran proveedor monolítico, tendríamos dos, bajo el mismo techo del componente proveedor colapsado. Seguiríamos teniendo el mismo estado y los mismos hijos, pero tendríamos un contexto separado. Tendríamos un contexto separado solo para los datos de estado y un contexto separado para la API con un montón de funciones solamente.

Ahora, como puedes ver, el contexto que contiene la API no depende del estado, por lo que su valor no cambiará con la actualización del estado y, como resultado, los componentes que utilizan esta API no se volverán a renderizar. Y ahora la gran pregunta aquí. ¿Deberíamos realmente usar el contexto en nuestras aplicaciones? Y la respuesta es tal vez no. En la vida real, lo más probable es que utilices alguna herramienta de gestión de estado como Redux en lugar de contexto. Pero el modelo mental y el comportamiento de los re-renders serán exactamente los mismos con esas herramientas, simplemente son más convenientes de usar. Así que en lugar de contexto, si entiendes el contexto, cualquier gestión de estado después de esto será pan comido.

6. El Atributo Key en React

Short description:

El atributo key en React suele ser malinterpretado. Se utiliza para identificar y diferenciar hijos en un array, permitiendo a React optimizar el renderizado. Contrario a la creencia popular, la key no evita los re-renderizados. En cambio, ayuda a React a determinar si un componente hijo es el mismo que antes o uno nuevo. Al asignar claves únicas, React puede actualizar y re-renderizar eficientemente los componentes cuando sea necesario.

Y ahora, el último mito sobre los re-renderizados, el atributo key. Este es mi favorito, probablemente el más utilizado y el menos comprendido en React. Lo utilizamos a diario cuando renderizamos arrays de elementos, y luego YesLint nos grita que la clave es obligatoria y que debemos poner algo allí. Pero, ¿qué sabemos realmente sobre esta clave, qué debería ir exactamente allí, para qué sirve? Si ahora mismo le preguntas a tus colegas estas preguntas, ¿cuál será la respuesta? Probablemente algo como esto. Dirían que la clave debe ser única y que no se debe usar el índice del array como clave. Pero, ¿por qué? ¿Qué tan única debe ser? ¿Puedo hacer algo como esto en su lugar? Y si no, ¿por qué no?

Pero si presionas un poco más con esas preguntas, a veces la gente dirá que necesitamos la clave para evitar los re-renderizados. Y este es el divertido. Aunque sí, la clave debe ser única, pero no tiene nada que ver con evitar los re-renderizados, por lo general. Esto es solo un mito. Como ya sabemos, si un componente se re-renderiza, todos sus hijos se re-renderizarán, ya sea que cambien las props o no, en este caso, y en este caso, si la clave está o no, en realidad no importa. Si el componente no está memorizado, se re-renderizará. La clave no lo evitará. Entonces, ¿por qué la necesitamos?

Imagínate en una habitación con un montón de gatitos idénticos que llevan diferentes sombreros. No conoces sus nombres, por lo que la única forma de identificarlos y describir qué gato lleva qué sombrero es por su número. El primer gato lleva una corona, el segundo lleva una gorra de béisbol, el tercero lleva un sombrero de mago. Esto es exactamente con lo que React tiene que lidiar cuando iteramos sobre un array y renderizamos hijos. Si no los nombramos explícitamente, React no tiene forma de saber cuándo renderiza este componente, si un hijo es el mismo hijo o uno completamente nuevo, aparte de usar sus posiciones en el array, que es lo que usa por defecto. A menos que nombremos esos hijos explícitamente. Para eso sirve la clave. Cuando asignamos esos atributos y el componente que tiene esos elementos se re-renderiza, React detectará que los hijos son exactamente los mismos que antes. Entonces, todo lo que necesitará hacer es re-renderizarlos como de costumbre. Sin embargo, si cambiamos la clave en uno de los hijos, React pensará que el antiguo componente con la clave igual a cero ya no está allí. Lo eliminará y lo desmontará. Luego detectará que aparece un nuevo componente con la clave número seis. Pensará que es un componente completamente nuevo. Entonces, lo montará desde cero. Como resultado, el resto de los componentes se re-renderizarán, pero el componente con el cambio de clave se volverá a montar.

7. El Rol de las Claves en la Prevención de Re-renderizados

Short description:

Incluso si nada más cambia excepto la clave, generar una nueva clave en cada re-renderizado resultará en remontar y varios errores. La clave no evita los re-renderizados; debemos usar métodos como react.memo. Usar arrays.index como clave es una mala práctica para listas dinámicas, ya que cada elemento se detectará como cambiado y se volverá a renderizar. En cambio, usar IDs únicos de los propios datos asegura que solo se monten los elementos nuevos. Para obtener más información, consulta los artículos y blogs sobre este tema.

Incluso si nada más cambia excepto la clave. Y eso debería responder a la pregunta de qué tan única debe ser nuestra clave. Si hacemos algo como esto, como generar una nueva clave en cada re-renderizado, entonces en cada re-renderizado, cada uno de ellos se volverá a montar. Esto resultará en varios errores con el enfoque y el estado. Sin mencionar que es mucho, mucho más lento que un re-renderizado normal, por lo que también tendrás problemas de rendimiento.

En cuanto a evitar los re-renderizados, la clave no tiene nada que ver aquí. Debemos usar uno de los métodos como react.memo para evitar que los hijos se vuelvan a renderizar. Si lo usamos, ahora el componente padre se vuelve a renderizar y los hijos no se volverán a renderizar. Pero no es por la clave, es por react.memo.

Eso plantea la pregunta, ¿cuál es el punto de la clave si no evita los re-renderizados? ¿Y por qué todos dicen que usar arrays.index como clave es una mala práctica? Es debido a las listas dinámicas. Imagina que agregamos un nuevo elemento al principio del array. Tal vez haciendo clic en un botón que agrega un nuevo elemento de tarea en la parte superior. ¿Qué sucederá aquí si usamos arrays.index como clave? El nuevo elemento se agrega al principio del array, por lo que ahora será el primero. Cada elemento en este array como resultado se detectará como cambiado. Y todos ellos se volverán a renderizar, incluso si están envueltos en react.memo. Sin embargo, si usamos IDs únicos que obtenemos de los propios datos, cuando agregamos el nuevo elemento al principio, solo se detectará como nuevo y se montará el primer elemento. El resto no cambiará. Y si están envueltos en react.memo, no se volverán a renderizar.

Hay mucho más que saber y entender sobre las claves, el contexto y los re-renderizados en general. Aquí tienes algunos artículos útiles que escribí sobre los temas. Y echa un vistazo al blog en sí. Tiene muchos, muchos más patrones de react e investigaciones como esta. Y si tienes alguna pregunta o comentario, no dudes en contactarme en Twitter o LinkedIn. Siempre estoy feliz de responder cualquier pregunta. Gracias.