Con eslint-config-next en un proyecto Next.js 16, eslint puede marcar como error código de refs que funciona en ejecución. Hay dos mensajes frecuentes. El primero aparece al leer una propiedad de un objeto que devuelve un hook personalizado y que contiene una ref:
error Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be
accessed outside of render, such as in event handlers or effects. [...]
> 6 | <div ref={panel.containerRef}>
| ^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
react-hooks/refs
El segundo, al asignar .current a una ref que llega como propiedad o como argumento:
error Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a
local variable instead.
Hint: If this value is a Ref (value returned by `useRef()`), rename the
variable to end in "Ref"
react-hooks/immutability
Los dos se corrigen sin desactivar las reglas. Las pruebas de este artículo se hicieron con Next.js 16.4.0, eslint-config-next 16.4.0 y eslint-plugin-react-hooks 7.1.1; el comportamiento exacto del análisis puede variar entre versiones.
Por qué pasa
La documentación de Next.js indica que eslint-config-next incluye los conjuntos de reglas recomendados de eslint-plugin-react-hooks, y la documentación de React lista refs e immutability entre las reglas del preset recommended. En el proyecto de prueba las reglas se ejecutaron sin que existiera ninguna configuración del compilador de React.
react-hooks/refsvalida que no leas ni escribasref.currentdurante el render, porque la ref puede no estar inicializada y su valor puede estar desactualizado. Para saber qué es una ref, el análisis usa tres señales: el valor viene deuseRef()ocreateRef(), es un identificador llamadorefo que termina enRefy accede a.current, o se pasa por la propiedad JSXref. Y esa inferencia sigue al valor a través de asignaciones, desestructuración y funciones auxiliares. Por eso un objeto que contiene una ref queda "contaminado": en la prueba, leerpanel.containerRef,panel.openypanel.toggledio un error por cada lectura.react-hooks/immutabilityvalida que no se modifiquen props, estado ni argumentos de hooks. Una asignación comoprops.forwardedRef.current = nodela marcó porque el análisis no reconoce ahí una ref. La misma asignación con una variable desestructurada llamadaforwardedRefno se marcó, porque el nombre termina enRef.
Solución
-
Ejecuta el linter directamente. En Next.js 16 el comando
next lintya no existe; se usa la CLI de ESLint:npx eslint . -
Si el hook crea la ref y la devuelve dentro de un objeto, crea la ref en el componente y pásala al hook. Código con el fallo:
export function useScrollPanel() { const containerRef = useRef(null); const [open, setOpen] = useState(false); return { containerRef, open, toggle: () => setOpen((v) => !v) }; } export function Panel() { const panel = useScrollPanel(); return ( <div ref={panel.containerRef}> <button onClick={panel.toggle}>{panel.open ? 'Cerrar' : 'Abrir'}</button> </div> ); }Código corregido, sin errores en la prueba:
export function useScrollPanel(containerRef) { const [open, setOpen] = useState(false); const scrollToTop = () => containerRef.current?.scrollTo({ top: 0 }); return { open, toggle: () => setOpen((v) => !v), scrollToTop }; } export function Panel() { const containerRef = useRef(null); const { open, toggle, scrollToTop } = useScrollPanel(containerRef); return ( <div ref={containerRef}> <button onClick={toggle}>{open ? 'Cerrar' : 'Abrir'}</button> <button onClick={scrollToTop}>Subir</button> </div> ); }Leer
containerRef.currentdentro de un manejador de eventos está permitido; lo que la regla evita es hacerlo mientras se renderiza. -
Si recibes una ref de un padre, no asignes
.currenta mano. Guarda el elemento en estado con una ref de callback y expónlo conuseImperativeHandle(en React 19,refllega como una propiedad normal):import { useImperativeHandle, useState } from 'react'; export function Contenido({ ref: forwardedRef, children }) { const [contentElement, setContentElement] = useState(null); useImperativeHandle(forwardedRef, () => contentElement, [contentElement]); return <div ref={setContentElement}>{children}</div>; }El setter de
useStatees una función, así que sirve como ref de callback. React llama auseImperativeHandlede nuevo cuandocontentElementcambia y asigna el nuevo valor a la ref del padre. -
Si el padre necesita el nodo en su efecto de montaje, usa una ref interna. Con el estado del paso anterior, la ref del padre vale
nulldurante el primer efecto de montaje, porque el elemento se guarda en un segundo render (se observa con una prueba sencilla). Esta variante también pasa el linter y entrega el nodo desde el primer montaje:export function Contenido({ ref: forwardedRef, children }) { const innerRef = useRef(null); useImperativeHandle(forwardedRef, () => innerRef.current, []); return <div ref={innerRef}>{children}</div>; }
Una alternativa más corta para el caso del hook: desestructurar en la misma línea (const { containerRef, open, toggle } = useScrollPanel()) no produjo errores en la prueba. Funciona por la heurística del análisis, así que es más frágil que pasar la ref como argumento.
Cómo verificar que quedó resuelto
npx eslint .
npx tsc --noEmit
eslint no debe mostrar ningún react-hooks/refs ni react-hooks/immutability. Después comprueba el comportamiento: con Testing Library, renderiza el componente con una ref de padre y comprueba que ref.current es el elemento div. En las pruebas hechas para este artículo, el elemento se entregó correctamente con las variantes de estado y de ref interna.
Variantes y errores relacionados
React 18 y forwardRef
La documentación de React indica que, desde React 19, ref está disponible como propiedad; en React 18 y anteriores hay que obtenerla con forwardRef. El cuerpo de las soluciones es el mismo: forwardRef(function Contenido({ children }, forwardedRef) { ... }).
Lecturas que sí están permitidas
Leer o escribir ref.current en efectos y manejadores de eventos es válido. La documentación de la regla también acepta la inicialización perezosa: if (ref.current === null) ref.current = crear(); durante el render.
Un objeto normal con una propiedad current
Según la documentación de la regla, el análisis trata como ref cualquier identificador llamado ref o terminado en Ref que lea o escriba .current. Si no es una ref de React, cambia el nombre (por ejemplo, caja). Si sí lo es, el mensaje sugiere lo contrario: renómbrala para que termine en Ref.
Desactivar la regla
La documentación de Next.js muestra cómo cambiar o desactivar reglas de react-hooks con la propiedad rules de eslint.config.mjs. Hazlo solo si entiendes el riesgo: las lecturas de refs durante el render son justo el tipo de código que puede producir pantallas desactualizadas.
Resumen
react-hooks/refsprohíbe leer o escribirref.currentdurante el render, y un objeto que contiene una ref también se considera "ref".- Crea la ref en el componente y pásala al hook, o desestructura el resultado del hook en la misma línea.
react-hooks/immutabilityprohíbe modificar props y argumentos; para combinar con una ref recibida usauseImperativeHandleen lugar de asignar.current.- Una ref de callback basada en estado deja la ref del padre en
nullhasta el segundo render; usa una ref interna si el padre la necesita al montar. - En Next.js 16 ejecuta
npx eslint .;next lintfue eliminado.
Comentarios
Publicar un comentario