Inicio / Artículos / Dónde mantiene su valor useState: Elementos de React frente a Fibers

Dónde mantiene su valor useState: Elementos de React frente a Fibers

Por qué un componente funcional de React olvida todo entre llamadas, por qué los elementos no pueden almacenar estado y cómo el campo memoizedState de Fiber mantiene vivos los valores de useState.

1724 palabras

Un componente funcional es simplemente una función, y las funciones olvidan sus variables locales en el momento en que devuelven un valor. Sin embargo, useState devuelve el valor actualizado en cada renderización, como si la función lo recordara. Este texto responde con precisión a una pregunta específica: ¿dónde reside físicamente ese valor entre las renderizaciones? Al final, podrás distinguir los dos objetos que React crea para cada componente, el elemento y la fibra, y explicar cuál almacena el estado y por qué.

El enigma: una función sin memoria

Comencemos con el componente más familiar que existe, un contador:

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Haz clic una vez y muestra 1, haz clic de nuevo y muestra 2. Nada sorprendente, hasta que lo miras como un simple JavaScript. Counter es una función. Cada vez que se invoca una función, sus variables locales se crean desde cero, y cuando esta devuelve el valor, estas desaparecen. Una segunda invocación comienza todo de nuevo.

Según esa lógica, la segunda vez que React llama a Counter(), la línea useState(0) debería producir nuevamente 0. En realidad produce 1. Algo fuera de la función debe estar guardando el número entre una llamada y otra. El cuerpo de la función no puede serlo, porque simplemente ese no es el modo en que funcionan las funciones. Entonces, ¿qué es?

Descartando el elemento

Hay un candidato obvio que resulta estar equivocado, y eliminarlo hace que la respuesta real quede más clara.

La línea <button onClick={...}>{count}</button> es JSX. Los navegadores nunca ejecutan JSX; un compilador como Babel o el compilador de TypeScript lo reescribe en una llamada a función normal antes de que el código sea distribuido. Conceptualmente, el resultado es:

React.createElement("button", { onClick: fn }, count)

Con la implementación automática del entorno de ejecución para JSX introducida en React 17, la llamada compilada es en realidad jsx() de react/jsx-runtime en lugar de React.createElement, pero ambas realizan la misma función. Sea cual sea el nombre de la función, esta devuelve un objeto ordinario:

{
  type: "button",
  props: { onClick: fn, children: 1 },
}

React denomina a esto un elemento. Se trata de una descripción, no de algo real: un pequeño registro que indica que aquí debe aparecer un botón, con este manejador de clic y mostrando este número. Los elementos reales contienen algunos campos adicionales, como key, ref y un marcador interno $typeof, pero ninguno de ellos almacena historial.

Ahora, la observación crucial. Cada llamada a Counter crea un elemento completamente nuevo. Al hacer clic en el botón, se ejecuta Counter(), se devuelve un objeto nuevo y el anterior se convierte en basura. Si el estado estuviera en el elemento, no habría forma de pasar de renderizar dos a renderizar uno, ya que el objeto de renderizado uno deja de existir cuando comienza el renderizado de dos.

Esa posibilidad de descartarlos es intencionada. Los elementos son económicos precisamente porque se reconstruyen desde cero en cada renderizado y nunca es necesario mantenerlos sincronizados con nada. Esto también significa que no pueden estar donde se almacena count.

El objeto que sobrevive: la fibra

Junto a los elementos, React mantiene otro objeto de mayor duración para cada componente montado. Este no se reconstruye en cada renderizado; se crea cuando el componente se monta por primera vez y luego se mantiene y actualiza mientras el componente permanece en la pantalla. Este es el fiber.

A menudo se describe a las fibras como algo misterioso, pero en su nivel más básico una fibra es un objeto JavaScript sencillo. No hay ninguna estructura especial en tiempo de ejecución detrás de ellas. Si pausas el depurador dentro del reconciler de React y examinas una, encontrarás un objeto ordinario con un conjunto de propiedades, del tipo que podrías crear tú mismo con un par de llaves. También puedes echarles un vistazo desde el navegador: React asigna claves internas a los nodos DOM que apuntan a sus fibras, y así es como React DevTools las localiza.

En lugar de listar todos los campos de una sola vez, resulta útil construir la fibra de Counter uno por uno. Inmediatamente después del montaje, una versión mínima se ve así:

{
  type: Counter,
}

type: de quién es la contabilidad

type es el campo más simple. Identifica qué componente sigue esta fibra. type: Counter significa que el objeto existe para gestionar una instancia de Counter, manteniendo una referencia a la función en sí.

También los elementos host reciben fibras. El botón que renderiza Counter tiene su propia fibra, y su type es la cadena "button" en lugar de una función. Por lo tanto, la fibra de <Counter /> tiene { type: Counter } y la fibra de <button> tiene { type: "button" }: mismo campo, misma finalidad, apuntando ya sea a tu componente o a una etiqueta integrada.

type también desempeña un papel en la reconciliación. Cuando React compara un nuevo elemento con una fibra existente en la misma posición, un type coincidente le permite reutilizar esa fibra y su estado, mientras que un type diferente hace que descarte la fibra antigua y cree una nueva. Por eso, al cambiar entre dos componentes diferentes en el mismo lugar se reinicia su estado.

No obstante, por sí solo, type no indica nada sobre cómo persiste el estado. Para eso se necesita el siguiente campo.

memoizedState: dónde realmente se almacena el conteo

useState necesita un lugar fuera de la función para guardar el valor actual, ya que las variables locales de la función se reinician en cada llamada. Ese lugar es un campo en la fibra denominado memoizedState. “Memoizado” simplemente significa recordado: se mantiene del valor anterior en lugar de volver a calcularlo.

Al agregarlo al boceto se obtiene:

{
  type: Counter,
  memoizedState: { count: 0 },
}

Esta es una representación simplificada. En la implementación real, memoizedState en la fibra de un componente funcional apunta al primero de una lista enlazada de objetos hook, uno por cada llamada a un hook, y el número 0 se encuentra en el propio campo memoizedState de ese hook en lugar de dentro de un objeto { count: 0 }. React no tiene idea de que tu variable se llama count; solo conoce “el valor del primer hook”. Para comprender dónde reside el estado, la versión simplificada es suficiente.

Ahora sigue un clic. React llama nuevamente a Counter(). Cuando la ejecución llega a useState(0), el hook no devuelve el 0 en tu código. Busca el estado almacenado de la fibra, encuentra el valor actual (que es 1 una vez procesado el clic) y lo devuelve. El argumento de useState es solo un valor inicial: se utiliza en la renderización inicial y luego se ignora. A partir de ese momento, la fibra es la fuente de verdad, no el valor literal dentro del cuerpo de tu función.

Esta es la respuesta completa al acertijo inicial. La cuenta no está en la función, que olvida todo entre llamadas, ni en el elemento, que se descarta después de cada renderización. Se encuentra en la fibra, un objeto separado que React mantiene y actualiza en cada renderización de Counter.

Dos consecuencias prácticas

Este modelo explica algunos comportamientos que, de otro modo, parecen arbitrarios:

  • Cambiar el argumento de useState después del montaje no tiene efecto en el valor almacenado, ya que React solo lo lee la primera vez. Si necesita restablecer el estado a partir de props, cambie la key del componente para que React cree una nueva fibra.
  • Dado que la fibra se busca por posición en el árbol, el estado pertenece al lugar donde se renderiza un componente, no a la definición de la función. Dos elementos <Counter /> uno al lado del otro obtienen dos fibras y dos conteos independientes.

Qué contiene la fibra además de estos dos campos

type y memoizedState son suficientes para resolver el problema del estado, pero una fibra real contiene mucho más. Guarda una referencia al nodo DOM que generó, punteros a su padre, primer hijo y siguiente hermano para que React pueda recorrer el árbol, así como las props anteriores para compararlas con las nuevas. Estos campos determinan cómo React identifica los cambios y cómo recorre todo el árbol de componentes, lo cual es un problema distinto a la ubicación de un valor en particular.

Lo que ahora se puede establecer con claridad es la línea divisoria entre estos dos objetos, ya que su confusión es la causa de muchas dudas sobre el proceso de renderizado:

Element                           Fiber
--------                          -----
Created by React.createElement    Created internally by React
New object every render           Same object, updated in place
Discarded right after             Persists for the component's
  React reads it                    entire mounted lifetime
Holds no history                  Holds memoizedState, the real
                                     remembered value across renders
Describes what should exist       Is the thing that actually exists,
                                     with real memory attached to it

Una forma concisa de recordarlo: un elemento es una solicitud que haces a React, mientras que una fibra es el registro propio de React de lo que existe actualmente. Cada renderización genera un lote nuevo de elementos económicos y sin memoria, y los entrega al árbol de fibras, que ya estaba presente desde la renderización anterior y contiene todo lo que debe permanecer. Un lado describe, el otro recuerda.

Un punto pendiente: las fibras vienen en pares

Afirmar que una fibra se “actualiza en su lugar” es una aproximación útil inicial, pero no es toda la verdad. En realidad, React mantiene dos versiones de cada fibra: una que coincide con lo que se muestra actualmente y otra que se prepara para la próxima actualización, cambiando entre ellas. Esa disposición permite a React trabajar en una actualización sin perturbar la interfaz visible, y merece una explicación aparte.

Puntos clave

  • Las variables locales de un componente funcional se recrean en cada llamada, por lo que la función misma no puede almacenar estado.
  • JSX se compila en llamadas que generan elementos: descripciones simples y desechables que se reconstruyen en cada renderizado.
  • Las fibras son objetos simples que persisten mientras un componente está montado; el type indica qué componente sigue una fibra.
  • useState lee y escribe valores a través de memoizedState de la fibra, que en realidad apunta a una lista de objetos de hook ordenados según el orden de las llamadas.
  • El valor inicial pasado a useState solo es importante en el momento del montaje; después de eso, la fibra es la fuente de verdad.

Lectura relacionada