Por qué existen las reglas de ganchos: fibra, listas de ganchos y despachadores
Un recorrido por los componentes internos de React: nodos Fiber, doble buffering, carriles, la lista enlazada de hooks y cómo cada familia de hooks integrados almacena su estado y programa sus tareas.
La mayoría de los desarrolladores de React pueden recitar las reglas de los hooks, pero muchos menos pueden explicar por qué su incumplimiento corrompe el estado en lugar de simplemente generar un error útil. La respuesta se encuentra en los mecanismos internos de React: cada llamada a un hook se convierte en un nodo en una lista enlazada almacenada en un Fiber, y React localiza cada nodo únicamente según el orden en que se realizan las llamadas a los hooks. Esta guía explica detalladamente el Fiber, el objeto hook, los despachadores entre renders y la mecánica interna de cada familia de hooks, para que las reglas dejen de parecer arbitrarias y comiencen a interpretarse como consecuencias del diseño.
No necesitarás este conocimiento para escribir un formulario o obtener datos. Sin embargo, resulta muy útil al depurar valores obsoletos, al decidir entre useEffect y useLayoutEffect, o al preguntarte por qué un hijo memorizado se vuelve a renderizar de todos modos.
Un resumen rápido: qué reemplazaron los hooks y las reglas que vinieron con ellos
Los hooks llegaron en React 16.8, y el conjunto integrado ha crecido hasta alcanzar unas diecisiete unidades. Resolvieron tres problemas crónicos de los componentes basados en clases: la lógica con estado era difícil de reutilizar entre componentes, la lógica relacionada estaba dispersa en los métodos de ciclo de vida hasta que los componentes se volvían excesivamente grandes, y las propias clases de JavaScript (el enlace de this, la comprensión de los ciclos de vida) confundían a muchos desarrolladores.
Con los hooks surgió un conjunto de reglas:
- Llamar a los hooks en el nivel superior del cuerpo de un componente funcional.
- Llamar a los hooks en el nivel superior del cuerpo de un hook personalizado.
- Nunca llamar a los hooks dentro de condiciones o bucles.
- Nunca llamar a los hooks después de un
returncondicional anticipado. - Nunca llamar a los hooks dentro de manejadores de eventos.
- Nunca llamar a los hooks en componentes basados en clases.
useEffect, useMemo o useReducer.try, catch o finally.Las infracciones generan advertencias, errores o, peor aún, bugs sutiles. La explicación breve es que los ganchos de un componente forman una lista enlazada simple conectada a un nodo Fiber, que es un objeto JavaScript con estado que React mantiene para cada componente. Para comprender por qué eso es importante, comience con Fiber en sí. Si ya lo conoce bien, pase directamente a la sección sobre el objeto de ganchos. Para una introducción más sencilla sobre la reconciliación y el estado, consulte crear un modelo mental para la reconciliación, el estado y los ganchos en React.
React Fiber: el motor en el que residen los ganchos
Fiber es el motor de reconciliación de React, introducido en React 16 como una reescritura completa de la forma en que React calcula y aplica las actualizaciones de la interfaz de usuario.
El problema con el reconciliador tradicional
Antes de Fiber, React utilizaba lo que a menudo se denomina reconciliador basado en pila. Con cada actualización, recorría el árbol de componentes de forma recursiva, y un recorrido recursivo en la pila de llamadas de JavaScript no puede detenerse a mitad de camino: una vez que comienza, sigue ejecutándose hasta que todo el árbol se procesa. En un árbol grande que monopolizaba el único hilo principal, las animaciones se congelaban, las teclas tardaban en responder y la interfaz funcionaba de forma intermitente. Peor aún, no había manera de permitir que una actualización urgente, como la presión de una tecla, pasara por delante de un proceso de renderizado grande y menos importante que ya estaba en curso.
Lo que hace posible Fiber
Una Fiber es un objeto de JavaScript simple que representa una unidad de trabajo, vinculada a una instancia de componente o a un nodo DOM. Dado que React sigue el rastro de estas unidades por sí mismo en lugar de depender de la pila de llamadas, adquiere tres capacidades:
- Pausar y reanudar. React puede detenerse a mitad de un proceso, permitir que el navegador maneje algo más urgente como la entrada del usuario y continuar posteriormente desde el mismo punto.
- Priorizar. Las actualizaciones urgentes pueden superar a las menos importantes.
- Reutilizar o descartar. Si el usuario navega a otra página mientras se está procesando un renderizado, el trabajo inconcluso puede simplemente descartarse.
La estructura de un nodo Fiber
Cada elemento de React, ya sea un componente, un elemento DOM host o un nodo de texto, cuenta con una Fiber correspondiente. Se trata de un objeto grande que contiene las propiedades del componente, su estado y un enlace a su representación en DOM.
En lugar de almacenar los hijos en arrays, las fibras forman un árbol a través de tres punteros:
childlleva al primer hijo de la fibra.siblinglleva a la siguiente fibra en el mismo nivel.returnlleva de vuelta al padre.
Procesar una actualización implica seguir estos enlaces: descender por child tanto como sea posible, moverse lateralmente mediante sibling, y subir de nuevo por return cuando se termina un ramo. Dado que se trata de un bucle ordinario sobre punteros y no de recursión, React puede detenerse entre cualquier dos fibras.
Bufereo doble con dos árboles
Fiber toma prestado el concepto de bufeo doble de la programación gráfica. En todo momento, React mantiene dos árboles de fibras en memoria:
- El árbol actual refleja exactamente lo que se muestra en la pantalla. React no lo modifica al calcular una actualización.
- El árbol en proceso se construye en segundo plano cuando algo cambia. React clona las fibras actuales que necesitan actualización y monta la nueva versión junto a la antigua.
Cuando el árbol en proceso está completo, React cambia el puntero raíz. El árbol en proceso se convierte en el actual, y la pantalla refleja el nuevo estado. Cada fibra mantiene un puntero alternate hacia su contraparte en el otro árbol, y así es como el estado de los hooks se transmite de una renderización a otra.
Fase de renderizado y fase de confirmación
Esta arquitectura divide cada actualización en dos fases.
La fase de renderizado es interrumpible. React recorre el árbol, llama a las funciones de tu componente, ejecuta los hooks y compara el resultado con el árbol actual mientras construye el árbol en proceso de elaboración en memoria. Dado que React controla el bucle de recorrido, su programador puede ceder el control al navegador cada pocos milisegundos. Si el usuario escribe mientras se está realizando un renderizado de baja prioridad, React puede pausarse, procesar la entrada y reanudarse. Incluso puede descartar todo el árbol en proceso de elaboración cuando una actualización más reciente y urgente lo haga obsoleto. Como un renderizado puede ejecutarse varias veces o no completarse nunca, las funciones de componente deben ser puras: no deben tener efectos secundarios durante el renderizado.
La fase de confirmación es síncrona. Una vez que se completa el árbol WIP, React aplica los cambios calculados al DOM real de una sola vez. Este paso no puede detenerse, ya que interrumpirlo a mitad de las mutaciones del DOM mostraría al usuario una interfaz parcialmente actualizada e inconsistente. Aquí se ejecutan los efectos de layout, desde aquí se programan los efectos pasivos y se adjuntan las refs.
Vías: cómo decide React qué interrumpir
Para saber qué puede interrumpir qué, React etiqueta cada actualización con una vía. Las vías se representan como bits en una máscara de bits, lo que hace que combinar y comparar prioridades sea sencillo. En general:
- Una vía de sincronización para interacciones discretas y urgentes como clics y presiones de teclas (los eventos continuos como el desplazamiento con el ratón o el scroll tienen su propia vía de alta prioridad).
- Vías de transición para tareas interrumpibles: actualizaciones en segundo plano, recálculos basados en datos y cambio de pestañas.
Nunca se interactúa directamente con Fiber, pero es la base de las características principales de React 18 y 19. El renderizado concurrente, Suspense, useTransition y useDeferredValue dependen todos de que el renderizado pueda ser interrumpido.
El objeto hook y por qué el orden de llamadas es fundamental
Los componentes funcionales no tienen una instancia para almacenar el estado, por lo que React lo guarda en la fibra en un campo llamado memoizedState. En el caso de los componentes funcionales, ese campo apunta al primer hook en una lista enlazada simple.
Cada llamada a un hook durante el renderizado corresponde a un objeto con una estructura más o menos similar a esta:
{
memoizedState: any, // The internal state of the hook
baseState: any, // The state before any unprocessed updates
baseQueue: Update | null, // Updates that were skipped due to priority
queue: UpdateQueue | null, // Circular linked list of pending state updates
next: Hook | null // Pointer to the next hook in the component
}
El memoizedState del hook almacena su valor (el estado de useState, el registro de efectos de useEffect, la pareja cachée de useMemo); queue guarda las actualizaciones pendientes; baseState y baseQueue registran las actualizaciones que se omitieron porque su canal no estaba siendo procesado; y next hace referencia al hook siguiente.
Observe lo que falta: no hay clave ni nombre. Al volver a renderizar, React recorre simplemente la lista desde el principio, emparejando la primera llamada a un hook con el primer nodo, la segunda con el segundo y así sucesivamente. Si se llama a un hook dentro de un if y la condición cambia, cada llamada posterior se empareja con el nodo incorrecto, y el estado de un hook se filtra al otro. Esa es la razón fundamental de las Reglas de los Hooks: garantizan que los mismos hooks se ejecuten en el mismo orden en cada renderizado. Las reglas sobre bucles, devoluciones anticipadas, bloques try y callbacks son todas variaciones de ese mismo requisito.
Una excepción moderna confirma este principio: la API use en React 19 puede llamarse de forma condicional, precisamente porque no depende de un slot en esta lista de la misma manera.
Dispatchers: el mismo nombre de hook, implementaciones diferentes
El useState que importas es un envoltorio simple. En tiempo de ejecución, redirige la solicitud al despachador que React tenga instalado para la fase actual. Las versiones antiguas de React exponen esto a través de ReactCurrentDispatcher; en las versiones más recientes, el despachador se encuentra en el objeto compartido interno de React, pero la idea es idéntica.
- HooksDispatcherOnMount está activo durante la primera renderización.
useStatese asocia conmountState, el cual asigna un nuevo objeto de hook, establece su estado inicial y lo agrega al final de la lista. - HooksDispatcherOnUpdate está activo durante las re-renderizaciones.
useStatese asocia conupdateState, el cual avanza a lo largo de la lista existente (en esenciaworkInProgressHook = workInProgressHook.next), procesa la cola pendiente y devuelve el nuevo estado.
Este diseño también explica por qué falla la llamada a hooks dentro de los manejadores de eventos: para cuando se ejecuta el manejador, ya se ha terminado el rendering y está activo el dispatcher que lanza errores.
Cómo funciona internamente cada familia de hooks
Todos los hooks comparten la base de lista enlazada, pero difieren ampliamente en lo que almacenan y cuándo se ejecutan sus operaciones.
Hooks de estado: useState y useReducer
Internamente, useState es en realidad useReducer con un reductor integrado que o bien devuelve el nuevo valor o llama a tu función de actualización con el valor anterior. Ambos comparten un mismo modelo de ejecución:
- Almacenamiento. El gancho mantiene un estado base (el valor más recientemente guardado) y una cola de actualizaciones, que es una lista enlazada circular de cambios pendientes.
- Distribución. Al llamar a un setter, por ejemplo
setCount(c => c + 1), se crea un objeto de actualización que contiene esa acción, se agrega a la cola y se marca la fibra como necesitada de procesamiento asignándole un canal (las versiones anteriores utilizaban tiempos de vencimiento con el mismo propósito). - Resolución. Durante la siguiente renderización, React recorre la cola y aplica cada acción en orden para generar el nuevo
memoizedState. Las actualizaciones cuyo canal no está incluido en la renderización actual se conservan enbaseQueuey se vuelven a procesar más tarde, lo que mantiene el orden entre las prioridades.
Esta es también la razón por la cual las funciones de actualización son la opción segura cuando el estado siguiente depende del anterior: se aplican en secuencia sobre cualquier estado que la cola haya calculado hasta el momento.
Ganchos de efecto: useInsertionEffect, useLayoutEffect y useEffect
Cada gancho de efecto almacena un registro de efecto en su estado, que contiene la función de configuración, la función de limpieza y el array de dependencias. Estos registros también se enlazan en una lista separada dentro de updateQueue de la fibra, y los efectos cuyas dependencias han cambiado se marcan para que la fase de confirmación sepa cuáles ejecutar. Los tres ganchos difieren en cuanto al momento de ejecución:
- useInsertionEffect se ejecuta antes de los efectos de maquetación, por delante de cualquier código que pueda leer la estructura del documento. Existe para las bibliotecas CSS-in-JS que necesitan insertar reglas
<style>temprano para que los estilos estén listos cuando se mida la maquetación, evitando así el recálculo repetido de los estilos. - useLayoutEffect se ejecuta de forma síncrona una vez que React ha modificado el DOM, pero antes de que el navegador tenga la oportunidad de dibujarlo. El hilo principal se bloquea hasta que finaliza el efecto y su limpieza, por lo que es adecuado para medir y ajustar los nodos del DOM antes de que el usuario vea algo, pero no para tareas más complejas.
MessageChannel y, como fallback, setTimeout), por lo que no retrasa la actualización visual. Tenga en cuenta que cuando la actualización proviene de una entrada discreta del usuario, React puede ejecutar los efectos pasivos antes del dibujo.Ganchos de rendimiento: useMemo y useCallback
Se trata de cachés que evitan cálculos costosos o mantienen estables las referencias entre renders.
- Almacenamiento. El gancho almacena una pareja: el valor en caché y el array de dependencias con el que se calculó.
- Ejecución. Al volver a renderizar, React compara cada nueva dependencia con la almacenada en caché utilizando
Object.is. Si todas coinciden, no se llama a la función de creación y se devuelve el valor almacenado en caché. Si alguna difiere, React llama a la función de creación, almacena el nuevo valor y las dependencias, y devuelve el resultado. - useCallback es equivalente a
useMemo(() => fn, deps): mantiene el objeto de función que pasaste en lugar del valor generado al invocarlo. La implementación original de React lo maneja por separado, pero el comportamiento es el mismo.
Dado que la comparación es superficial, una dependencia que sea un objeto o array recién creado en cada renderizado anula por completo el efecto del caché.
Ganchos para valores mutables: useRef y useImperativeHandle
Los refs almacenan información que no se utiliza para el renderizado, como un nodo DOM o un ID de tiempo de espera.
- useRef es el gancho más simple del código. Al montarse, crea
{ current: initialValue }y lo almacena como estado del gancho; cada render posterior devuelve el mismo objeto. Escribir encurrentnunca afecta a la cola de actualizaciones ni a las vías de procesamiento, por lo que nunca se dispara un render. - useImperativeHandle personaliza lo que ve un padre a través de un ref al adjuntar sus propios métodos a él. Internamente se comporta como
useLayoutEffect: se ejecuta de forma síncrona durante el proceso de confirmación, de modo que el handle está listo para cuando se ejecutan los efectos del padre. En React 19, los componentes funcionales pueden recibirrefcomo un prop normal, por lo que ya no se necesitaforwardRefpara usarlo.
El gancho de contexto: useContext
useContext destaca porque nunca ocupa un lugar en la lista de ganchos.
- Lectura. Lee el valor del Proveedor más cercano por encima del componente y registra ese contexto en la lista de dependencias de la fibra.
- Propagación. Cuando el valor de un Proveedor cambia, React busca debajo de él las fibras cuyas dependencias incluyen ese contexto y programa su vuelta a renderizar. Esto ocurre incluso si un componente intermedio evita el proceso mediante
React.memooshouldComponentUpdate, por lo que memorizar a un padre no protege a los consumidores de contexto.
Ganchos concurrentes: useTransition y useDeferredValue
Este par le permite controlar el sistema de carriles, lo que permite interrumpir renders prolongados.
- useTransition devuelve
[isPending, startTransition]. Las actualizaciones realizadas dentro destartTransition(() => setQuery(text))reciben una ruta de transición en lugar de una urgente. Si se produce un clic o una tecla mientras se renderiza la transición, React abandona el árbol en proceso de actualización, procesa la actualización urgente y luego reinicia la transición desde cero. - useDeferredValue envuelve un valor en lugar de un setter. React mantiene efectivamente dos versiones: primero renderiza con el valor anterior para que la pantalla siga siendo responsive, y luego programa un renderizado en segundo plano de baja prioridad con el nuevo valor.
Gancho especializados: useId y useSyncExternalStore
Algunos gancho aparecen raramente en el código de las aplicaciones, pero son esenciales para los autores de bibliotecas.
- useId evita incoherencias en la hidratación durante el renderizado del lado servidor. Deriva un ID a partir de la posición del componente en el árbol. Dado que la estructura del árbol es la misma en el servidor y en el cliente durante la hidratación inicial, los IDs coinciden sin necesidad de un contador global.
- useSyncExternalStore sustituye las suscripciones manuales con
useEffecta almacenes externos como Redux o Zustand. Se le proporcionan dos funciones: una que registra un oyente de cambios en el almacén ygetSnapshot, que devuelve el valor actual del almacén. React lee la instantánea durante el renderizado y, si el almacén cambia mientras se está renderizando, vuelve a renderizar de forma síncrona para que ninguna parte de la interfaz muestre una versión diferente del almacén que otra. Esto evita los problemas de desalineación que podría causar el renderizado concurrente.
Puntos clave
- El estado de los ganchos es una lista enlazada dentro de la fibra, determinada únicamente por el orden de las llamadas; cada regla relacionada con los ganchos existe para mantener ese orden idéntico en todas las renderizaciones.
- Fiber convierte el proceso de renderizado en un bucle interrumpible, y el doble buffering permite a React preparar un nuevo árbol sin modificar lo que se muestra en la pantalla.
- La fase de renderizado puede ejecutarse muchas veces y debe permanecer pura; la fase de confirmación se ejecuta una sola vez, de forma síncrona, y es donde se activan los efectos.
- Los despachadores explican tanto la separación entre montaje y actualización como el error que ocurre cuando se llama a un gancho fuera del proceso de renderizado.
- Saber dónde almacena cada gancho sus datos y cuándo se ejecuta su lógica facilita elegir el momento adecuado para aplicar efectos, mantener la memorización eficaz y comprender mejor el contexto así como las actualizaciones concurrentes.
Lecturas relacionadas
- Donde mantiene su valor useState: React Elements versus 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.
- Tipado de React Hooks: useState, useEffect, useReducer y gancho personalizados — Aprenda cómo tipar correctamente useState, useEffect, useReducer y los gancho personalizados en TypeScript, además de cuándo elegir TypeScript en lugar de JavaScript puro realmente trae beneficios.