Video Summary and Transcription
Esta charla explora cómo funcionan los useState bajo el capó y por qué es importante entenderlo. Aborda la confusión común en torno a la devolución de llamada a setState y proporciona ideas obtenidas al explorar el código fuente de los React hooks. Saber cómo funcionan los useState es importante para aprender patrones, depurar y ganar confianza en nuestro código. React gestiona el valor actual de los hooks en una lista enlazada y realiza actualizaciones de forma secuencial. React optimiza el renderizado mediante el almacenamiento en caché de cálculos y la realización de renderizados superficiales cuando el estado no cambia.
1. Introducción a useState
Esta charla explora cómo funciona useState por debajo y por qué es importante entenderlo. Aborda la confusión común en torno al callback de setState y proporciona ideas obtenidas al explorar el código fuente de los hooks de React.
Hola, soy Adam Klein, y esto es No Sabemos Cómo Funciona useState. Esta charla nació después de escuchar muchas veces a la gente hacer esta pregunta en diferentes foros. ¿Qué pasa con el callback de setState? Así que la gente estaba migrando de usar clases y llamar a setState, a usar hooks y llamar useState. Y echaban de menos esta función de poder agregar un callback después de que se actualice el estado. La gente preguntaba, ¿cómo hago eso con los hooks? Lo que me hizo darme cuenta de que la gente simplemente no sabe cómo funciona useState por debajo. Yo tampoco sabía cómo funcionaba useState por debajo hasta que empecé a notar algunas cosas extrañas al hacer console logs. Y decidí abrir el código fuente de los hooks de React. Y descubrí algunas cosas geniales por debajo. Lo que me hizo darme cuenta de que si la gente supiera esto, no preguntarían sobre el callback de setState.
2. Importancia de entender useState
Saber cómo funciona useState por debajo es importante por varias razones: es divertido, nos permite aprender patrones que se pueden aplicar a otros proyectos y ayuda con la depuración y a ganar confianza en nuestro código.
Antes de sumergirnos, preguntémonos, ¿por qué necesitamos saber cómo funciona por debajo? Quiero decir, lo estamos usando, sabemos cómo usarlo. Entonces, en mi opinión, hay tres razones. En primer lugar, es divertido. Es divertido saber y obtener conocimiento adicional sobre el framework que usamos todos los días. Desmitifica la magia. La segunda cosa es que podríamos ver patrones de los que podemos aprender e implementar en nuestros propios proyectos, incluso si no están relacionados con React. Y tercero, creo que cuando estás depurando código y cosas extrañas suceden, es muy útil saber cómo funciona por debajo y ganar más confianza sobre el código que escribimos.
3. Entendiendo setCounter
Vamos a explorar qué sucede cuando llamamos a setCounter en un componente simple que utiliza useState. React gestiona el valor actual de los hooks en una lista enlazada y desencadena una nueva renderización después de actualizar el valor del hook. Sin embargo, esta comprensión común es incorrecta.
Entonces, tomemos este código, que es un componente simple, y utiliza un estado llamado contador, y tenemos un botón con un controlador de eventos onClick. Y en el controlador de eventos onClick, llamamos a setCounter y pasamos una función actualizadora.
Ahora, esto también podría ser un valor, pero como sabes, useState también admite funciones actualizadoras, que reciben el valor anterior y devuelven un nuevo valor. Así que vamos a profundizar en lo que sucede cuando llamas a setCounter. Y esto es lo que solía pensar.
Ahora sé, y tú también podrías saber, que React gestiona el valor actual de los hooks para nosotros en una estructura de datos ordenada, que es una lista enlazada de hooks, y se llaman en orden. Así que lo que pensaba que sucedía es que llamamos a setCounter. React ve que pasamos una función actualizadora, por lo que toma el valor actual, que es cero, lo pasa a la función actualizadora, obtiene el nuevo valor, que en nuestro caso es uno, actualiza el valor actual del hook a uno y luego desencadena una nueva renderización de nuestro componente, y nuestro componente llama a useState de nuevo y obtiene el nuevo valor, que React almacena, que es uno. Así que es bastante simple. Lo único es que está equivocado. Esto no es lo que sucede.
4. Entendiendo setCounter en React
Cuando llamamos a setCounter en React, la función de actualización se agrega a una cola y se programa una nueva renderización. El componente se vuelve a renderizar después de que se procesen todas las actualizaciones. useState verifica las actualizaciones en cola durante la renderización y las realiza de manera secuencial. Esto explica el orden de renderizado y actualización en los componentes de React.
Antes de mostrarte cómo sucede, veamos algunos registros en la consola. Aquí tenemos un componente que hace prácticamente lo mismo, y veamos qué sucede cuando hacemos clic en el botón de suma. Tenemos un controlador de eventos onclick, y llamamos a setCounter. Minimicemos esto con la función de actualización. Aquí tenemos un registro en la consola de la actualización, y aquí tenemos un registro en la consola del renderizado. Y cuando hacemos clic en el botón de incremento, déjame mover esto aquí, primero se renderiza, y luego llamamos a la función de actualización, que es exactamente lo contrario de lo que esperamos, ¿verdad?
Esperamos que actualice el valor del gancho y luego se vuelva a renderizar, pero es al revés. Así que veamos cómo sucede esto. Cuando llamamos a setCounter, en realidad hay otra lista enlazada adjunta a cada uno de nuestros ganchos, y esta es la cola de actualización. Entonces, cada actualización que realizamos en nuestro estado no se realiza de inmediato. Se agrega a la cola y se procesará durante la renderización. Veamos cómo funciona esto.
Llamamos a setCounter, pasamos la función de actualización. Todo lo que hace React es poner esta función de actualización en una cola y luego programa una nueva renderización, lo que significa que activa algún tipo de indicador que dice que este componente debe volver a renderizarse después de que terminemos todas las actualizaciones. Ahora podríamos tener más actualizaciones. Podríamos actualizar este gancho nuevamente. Podríamos actualizar otros ganchos. Podríamos llamar al callback del componente padre, que actualizará algunas cosas. Podríamos despachar alguna acción de Redux que actualizará otros componentes. Y todas estas actualizaciones se encolarán. Y luego, en algún momento, y discutiremos cuándo sucede. React decide volver a renderizar todo el árbol de componentes, o no todo el árbol de componentes, los componentes que se programaron para volver a renderizarse de arriba hacia abajo. Según la jerarquía del árbol de componentes. Y como mencioné, discutiremos cuándo sucede esto más adelante. Y ahora, y solo ahora, cuando realmente volvemos a renderizar el componente, React mira lo que llamamos useState. Cuando realmente llamamos a useState, durante la renderización, React verifica si hay alguna actualización en cola, y solo entonces las realizará. Así que durante la renderización, mientras llamamos a useState de este gancho específico, React buscará y verá, bien, hay una acción, vamos a realizarla, esta acción actualiza el gancho a uno, y si hay otras acciones, las realizará de manera secuencial hasta que agotemos la cola. Agreguemos otro registro en la consola, minimicemos esto. Ahora, como puedes ver, agregamos otro registro en la consola después de useState. Así que registramos antes de useState, después de useState y durante la función de actualización, y hagamos algunas actualizaciones, borremos el registro, y cuando hagamos clic en más, puedes ver que lo primero que sucede es que se renderiza. Lo segundo es que llamamos a la actualización, lo que significa que durante la llamada a useState, React ejecuta la función de actualización, y solo entonces llamamos a después de useState, que está aquí.
5. Actualizando el Estado y Simulando setState
Cada actualización de un gancho se encola y se realiza de forma perezosa durante la invocación del gancho. El estado se actualiza solo durante el renderizado. useEffect o useLayoutEffect se pueden utilizar para realizar acciones después de que se actualice el estado. Esta es la forma correcta de simular el comportamiento de la función de actualización de setState.
Siempre es bueno ver por ti mismo lo que la gente te enseña. Entonces, para resumir esta parte de la charla, cada actualización de un gancho se encola. Después de que se realicen todas las actualizaciones, los componentes se vuelven a renderizar y las actualizaciones se realizan de forma perezosa durante la invocación del gancho. Perezosamente significa que React las realiza solo en el momento exacto en que se necesitan, y no antes.
Entonces ahora podemos, en primer lugar, responder a esta pregunta. ¿Cómo llamamos al callback de setState cuando usamos useState? Ahora podemos responder, ¿cuándo se actualiza el estado? Solo durante el renderizado. Entonces, ¿cómo podemos hacer algo después de que se actualice el estado? Llamamos a useEffect o useLayoutEffect, que es algo que se realiza después de que ocurra el renderizado. De acuerdo. Y obviamente podemos pasar este estado como una dependencia del useEffect para que podamos saber cuándo se actualizó exactamente este estado, y podemos react a ello. Entonces, esta es la forma estándar de simular el comportamiento de la función de actualización de setState. Es la forma correcta de hacerlo.
6. Entendiendo renderBailout
React calcula ansiosamente el nuevo estado antes de desencadenar un nuevo renderizado. Si el nuevo estado es diferente, se cancela el renderizado. React optimiza el tiempo de renderizado mediante heurísticas. El nuevo valor calculado se almacena en la cola de actualizaciones, evitando el recomputo. La primera vez que se actualiza un valor, React vuelve a renderizar si se devuelve un nuevo valor. Las actualizaciones posteriores descartan esta optimización.
¿Qué pasa con renderBailout? Entonces aquí, digamos que tenemos nuevamente un contador, pero lo establecemos en el mismo valor exacto. Ahora esperamos que React sepa que es el mismo valor y no desencadene un nuevo renderizado de nuestro componente. Pero ya dijimos que React solo ejecuta las actualizaciones durante el renderizado del componente, entonces ¿cómo puede cancelar o volver a renderizar el componente si calcula todo de forma perezosa? Ni siquiera sabe que lo establecimos en el mismo valor.
Así que hice trampa. En realidad, a veces ocurre otro paso, que es que React calcula ansiosamente el nuevo estado antes de desencadenar un nuevo renderizado. Y si determina que el nuevo estado es diferente al estado anterior, cancelará el renderizado y no desencadenará un nuevo renderizado. Obviamente, no lo hará si ya programamos un nuevo renderizado para este componente. No es necesario hacer este cálculo ansioso, porque de todos modos necesitamos volver a renderizar. Y a veces, React determinará que no vale la pena hacer el cálculo. Por ejemplo, si ya hemos llamado al mismo callback varias veces y cada vez devuelve un nuevo valor, React asume que devolverá un nuevo valor. Entonces son solo heurísticas que React ejecuta para optimizar nuestro tiempo de renderizado.
Entonces, obviamente, si el nuevo valor calculado es diferente, React programará un nuevo renderizado y continuará el flujo, excepto que como ya calculó el nuevo valor, almacenará el resultado en la cola de actualizaciones. Entonces, cuando realmente llamemos a useState y realicemos la cola de actualizaciones, React ya tendrá este resultado dentro de la cola. Aún no ha actualizado el valor real del gancho, pero ha almacenado el resultado de esta acción específica. Llamamos a la acción, no tenemos que calcularla nuevamente porque el resultado está en cola. Actualizamos el valor del gancho y continuamos con el renderizado. Entonces, la razón por la que digo que hice trampa es porque cada vez que te mostré los registros de la consola, hice clic en el botón de suma varias veces, para evitar mostrarte este comportamiento extraño que ocurre la primera vez que actualizas el valor.
Entonces, la primera vez que hice clic, puedes ver que lo primero que sucede es que React intenta actualizar el estado. Llama a la función de actualización. Entonces, por primera vez, React dice, veamos qué sucede. Y hace el cálculo ansioso y ve que el nuevo valor es igual al valor anterior y vuelve a renderizar los componentes, y solo entonces muestra después de useState. Ahora, la razón por la que vuelve a renderizar es porque en realidad devolvimos un nuevo valor. Entonces teníamos cero y el nuevo valor es uno. Entonces dice, está bien, necesitamos volver a renderizar el componente. Y luego, cuando llamamos a useState, uno ya está en caché. Entonces React no llama a la función de actualización nuevamente. Y luego llegamos a después de useState. Esto es para la primera vez. La segunda vez, React ya descarta esta optimización, porque la última vez que llamamos a la función de actualización, devolvió un nuevo valor. Entonces simplemente asume que la próxima vez también devolverá un nuevo valor.
7. Optimización y Renderizado Superficial en React
React optimiza el proceso de renderizado calculando de forma ansiosa y almacenando en caché el cálculo. Si el estado no cambia, React realiza un renderizado superficial y detiene cualquier otro re-renderizado. Este comportamiento se explica en la documentación de React.
Entonces, no sé exactamente cómo React optimiza esto. No vi ningún otro patrón. Obviamente, es algo que pueden cambiar todo el tiempo. Por lo tanto, no es parte de la documentación y no puedes confiar realmente en cómo funcionará esta optimización. ¿Y qué pasa si calculamos el estado de forma perezosa? Pero aún así no cambia. Entonces React no hizo el cálculo ansioso. Lo hizo de forma perezosa, pero aún así no cambió. ¿Entonces sigue re-renderizando todo, aunque lo único que sucedió fue que actualizamos el estado al mismo valor? Una pregunta larga, respuesta corta, no. No seguirá re-renderizando. Solo realizará un renderizado superficial de este componente y si descubre que en realidad nada cambió, se detendrá allí. También puedes consultar la documentación de React que explica exactamente esto. Entonces, para resumir esta parte, React a veces calcula de forma ansiosa para evitar el renderizado. Almacena en caché el cálculo ansioso para evitar volver a calcularlo. Y de todos modos, si el cálculo se realiza de forma perezosa y el estado no se actualiza, se evitará el renderizado y simplemente se completará el renderizado superficial de nuestro componente.
8. ¿Cuándo se vuelve a renderizar React? ¿Qué pasa con los reductores?
¿Cuándo se vuelve a renderizar React? React decide volver a renderizar después de que se completa la actualización. Los controladores de eventos en React se ejecutan en lotes y cada actualización de un hook se encola. Las funciones asíncronas desencadenan re-renderizados inmediatos. Para evitar parpadeos y optimizar los renderizados, utiliza controladores de eventos síncronos o react-dom unstableBatchUpdates. Se produce un error en las versiones antiguas de React al usar eventos reciclados dentro de funciones actualizadas. Actualiza a nuevas versiones o extrae los valores antes de usarlos.
Parte tres. ¿Cuándo se vuelve a renderizar React? Así que te prometí que explicaría cuándo sucede esto, así que programamos el re-renderizado, pero ¿cuándo decide React realmente, ok, hemos terminado con la actualización, vamos a volver a renderizar? Así que nuevamente, pongamos un ejemplo. Tenemos un controlador de eventos onclick. Y cada controlador de eventos en React se ejecuta dentro de un lote, lo que significa que cuando llamamos a este onclick, React lo ejecuta dentro de alguna función de lote internamente. Y cada actualización de un hook se encola. Así que la primera vez que llamamos a setCounter, agregará una acción a la cola. La segunda vez que llamamos a setCounter, agregará una acción a la cola. Y después de que la función termine, desencadenará un re-renderizado. Así que probablemente te preguntes, ok, pero ¿qué pasa con las funciones asíncronas? Si llamamos algo asíncrono, entonces la función real se completa antes de llegar al código asíncrono. Así que React realmente no puede saber que estás intentando ejecutar actualizaciones en el estado. No sabe cuándo se completará esta función asíncrona. Así que el lote se completa de inmediato y luego cada llamada a setCounter desencadenará un re-renderizado del componente. Así que en este caso, con una función asíncrona, en realidad volveremos a renderizar el componente dos veces. Así que esto es importante saberlo en caso de que quieras evitar algún tipo de parpadeo en la interfaz de usuario, o si quieres optimizar tus renderizados. Y luego debes saber que esto desencadenará un re-renderizado dos veces. Por lo general, no es un problema, pero ayuda a ser consciente de la diferencia entre funciones síncronas y funciones asíncronas. Y si realmente quieres hacer esto dentro de un lote, puedes llamar a react-dom unstableBatchUpdates. Y esto hará el mismo comportamiento que React hace cuando realmente llamas a un controlador de eventos síncrono. Y cuando llamas a unstableBatchUpdates, solo se renderizará una vez, aunque hayas actualizado el contador dos veces. Lo único es que esta es una función llamada unstable guion algo, obviamente nos disuade de usarla. Pero es bastante seguro usarla si realmente necesitas, porque React, el equipo se da cuenta de que mucha gente la usa y algunas bibliotecas y frameworks la usan. Así que en realidad la declararon no como parte de la API pública, pero no hacen cambios que rompan esta API porque se dan cuenta de que mucha gente la usa. Y también van a admitir en las nuevas versiones de React, actualizaciones en lotes de una manera más estable. Así que el paso adicional es, ¿dónde está el error? Esto es más relevante para versiones anteriores de React, pero muchos de ustedes probablemente todavía las están usando. ¿Cuál es el error con este código? Tenemos un controlador de eventos y llamamos a setstep, pasando una función actualizada y usamos e.target.value. El problema con este código, nuevamente, en versiones anteriores de React, es que estamos usando E, que es un evento, y React recicla los eventos, lo que significa que, y ahora sabemos que esta función actualizada solo se ejecutará durante el próximo re-renderizado, donde React ya podría haber reutilizado el evento sintético que se está utilizando dentro del controlador de eventos. Entonces, la forma de resolver esto es, en primer lugar, actualizar a nuevas versiones de React, cuando no tengas este problema. Y el segundo resultado, si no puedes migrar, es simplemente extraer el valor antes y luego usarlo dentro de la función actualizada, o llamar a persistEvent, que persiste el evento y evita que se vuelva a renderizar. Entonces, para resumir esta parte, React realiza los controladores de eventos en lotes, el código asíncrono no se ejecutará en lotes a menos que usemos actualizaciones en lotes inestables o las nuevas API de React, y los eventos se reciclan, no los uses dentro de las funciones actualizadas en versiones antiguas de React.
Parte 4, ¿qué pasa con los reductores? La respuesta es bastante simple, el reductor es en realidad, el estado, es en realidad un reductor bajo el capó.
9. Understanding setState and Reducer
La función setState es una función de despacho que toma el valor o la función actualizada como argumento. El reductor en setState, llamado basicStateReducer, utiliza el estado anterior y la acción para determinar si la acción es una función o un nuevo valor. Si es una función, llama a la función con el estado anterior y devuelve el resultado. De lo contrario, devuelve el nuevo valor. Este comportamiento se aplica tanto a useState como a useReducer.
Y la función setState es en realidad una función de despacho, estamos despachando el valor o la función actualizada, y el reductor de setState simplemente toma el valor y lo utiliza en el valor anterior. Así que este es el código real de React, este es el reductor que useState utiliza, se llama basicStateReducer, toma el estado anterior y la acción, y luego verifica si la acción es una función, lo que significa que pasamos una función actualizada, simplemente llama a esta acción con el estado anterior y devuelve el resultado, de lo contrario, simplemente pasamos el nuevo valor, por lo que devuelve el nuevo valor. Como puedes ver, verificamos si es una función, si es una función la llamamos, de lo contrario, simplemente devolvemos el valor. Lo que significa que todo lo que dijimos sobre cómo funciona useState, se aplica exactamente de la misma manera a useReducer.
10. Reducer Execution and Render Cycle
Si despachamos una acción, el reductor no siempre se ejecutará. Hay escenarios en los que el ciclo de renderizado puede interrumpirse o el componente puede desmontarse antes del ciclo de renderizado. En tales casos, las actualizaciones en cola se descartan y el reductor no se ejecuta. Por lo tanto, si no ves el registro en la consola dentro del reductor después de despachar una acción, esta podría ser la razón.
Y ahora otra pregunta, si despachamos una acción, ¿el reductor siempre se ejecutará? ¿Podemos garantizar que el reductor se ejecutará si despachamos una acción? La respuesta es no, porque solo llamamos al reductor durante el ciclo de renderizado. Es posible que ni siquiera lleguemos al ciclo de renderizado. En primer lugar, puede haber alguna excepción que detenga el renderizado a la mitad. Y en segundo lugar, nuestros elementos pueden incluso desmontarse en el próximo ciclo de renderizado, porque recuerda, llamamos a setState, pero también podemos llamar a algún tipo de devolución de llamada en nuestro componente padre. Y tal vez esta devolución de llamada cambie algún tipo de indicador que oculte nuestro componente en el próximo ciclo de renderizado. Por lo tanto, es posible que ni siquiera llamemos a la función de renderizado de este componente nuevamente, lo que significa que simplemente descartamos la cola de las actualizaciones y el reductor ni siquiera se ejecutará. Entonces, nuevamente, si despachas una acción y no ves el registro en la consola dentro del reductor, ahora sabes por qué.
11. Actualizaciones Asincrónicas de Estado en React
Las actualizaciones de estado en React pueden ser asíncronas o diferidas dependiendo del contexto. Al llamar a setCounter, la actualización puede ocurrir de forma síncrona o asíncrona, dependiendo del modo concurrente y el enlotado. El proceso de renderizado en React puede retrasarse o diferirse para priorizar otras tareas. Esta complejidad puede generar confusión sobre la naturaleza síncrona o asíncrona de las actualizaciones de estado.
Entonces, para resumir, el estado es un reductor, todo lo que dijimos sobre el estado se aplica a los reductores. Y la siguiente parte es un poco filosófica. La gente sigue diciendo que las actualizaciones de estado son asíncronas en React. Y si buscas en Google, obtendrás muchas respuestas diferentes, que determinan que setState es asíncrono, lo cual es bastante cierto, pero hay algunos matices que debes conocer.
Así que, como todo en Facebook, es complicado. Las actualizaciones de estado pueden ser asíncronas. Veamos algunos registros de consola y describámoslo de la mejor manera posible. Ahora quiero mostrarte un controlador de eventos onClick. Llamo a resolve de una promesa y console.log. Esto se registrará inmediatamente después del ciclo de eventos actual, lo que significa después de todas las tareas síncronas. Luego llamamos a setCounter y llamamos a afterUpdates. Borremos el registro y veamos qué sucede cuando llamamos a plus. Como puedes ver, llamamos a setCounter dos veces. Lo registramos dos veces y estas son las primeras cosas que registramos en nuestra consola. Tiene sentido porque ocurrieron de forma síncrona.
También puedes ver que el renderizado ocurrió aquí y ocurrió antes del código asíncrono, que estaba aquí. Lo que significa que durante el mismo ciclo de eventos, React llamó sincrónicamente a nuestro controlador de eventos, desencadenó la actualización de renderizado y llamó a nuestra función de renderizado del componente. Todo se realizó de forma síncrona dentro del mismo ciclo de eventos. Y solo después de que se completó el renderizado, vimos este registro asíncrono de la promesa. Esto nos confunde un poco, porque acabamos de decir que las actualizaciones de estado son asíncronas, pero acabamos de ver que ocurren de forma síncrona.
Entonces, hay algunas respuestas a esta pregunta. En primer lugar, en el modo concurrente, podría ser asíncrono en un ciclo de eventos diferente. Porque React puede retrasar el renderizado de nuestros componentes a favor de tareas más prioritarias. Por lo tanto, podría realizarse verdaderamente de forma asíncrona. Y cuando hacemos enlotado, puedes verlo como diferido. No es verdaderamente asíncrono en términos del ciclo de eventos, pero está diferido. Llamamos a setCounter y luego pasamos a nuestra siguiente instrucción. Y los renderizados aún no han ocurrido. Parece asíncrono. Nuevamente, esta es una explicación muy complicada.
12. Comprendiendo las Actualizaciones de Estado
Las actualizaciones de estado en React pueden considerarse asíncronas. Se encolan y se calculan de forma perezosa durante el renderizado. React abandona el renderizado si el estado no cambia. Las actualizaciones se agrupan dentro de los controladores de eventos y el estado es en realidad un reductor.
Es mucho más fácil pensar en ellas como asíncronas, y esto es básicamente lo que React nos recomienda. En primer lugar, debido al modo concurrente, donde realmente puede ser asíncrono. Y en segundo lugar, debido a los detalles de implementación. No quiere que confiemos en el hecho de que ocurre durante el mismo ciclo de eventos. Por lo tanto, la mejor manera de simplificarlo es pensar en las actualizaciones de estado como asíncronas.
Para resumir esta parte, las actualizaciones de estado se encolan y se calculan de forma perezosa durante el renderizado. React abandona el renderizado si el estado no cambia. Las actualizaciones se agrupan dentro de los controladores de eventos y el estado es en realidad un reductor. Y por último, las actualizaciones de estado deben considerarse asíncronas.
Gracias. Espero que hayas disfrutado de la charla. Yo ciertamente lo hice, y espero que hayas aprendido cosas interesantes. Y si hay alguna pregunta, avísame. Nos vemos la próxima vez. Adiós.
Comments