Mejores Prácticas de React Hooks
Un resumen condensado de las 25 mejores prácticas más importantes extraídas de cada página en esta sección.
- Prefija los Hooks Personalizados Con use: Comienza el nombre de cada hook personalizado con
usepara que el plugin ESLint rules-of-hooks realmente ejecute las reglas; sin el prefijo, las llamadas condicionales ilegales siguen rompiendo React pero ningún error de linting las detecta. - Retorna Tuplas Con as const: Devolver
[value, setValue] as constproporciona a los consumidoresreadonly [T1, T2]con tipos de desestructuración correctos; sinas const, TypeScript amplía a(T1 | T2)[]y se pierde el orden de la tupla. - Importa useActionState Desde react: En React 19, usa
useActionStatedesde"react"— no el deprecadouseFormStatedesde"react-dom"— y recuerda que la nueva firma devuelve[state, formAction, isPending]con estado pending incluido. - Dispersa prevState en Reducers de Acciones: La firma de la acción
(prevState, formData) => newStatereemplaza el estado completamente, así que dispersa...prevStatecuando solo quieras fusionar un campo; olvidar esto borra cada campo sin tocar en cada envío. - useCallback Solo Ayuda a Consumidores Memoizados:
useCallbackes inútil por sí solo — previene re-renderizados solo cuando un hijoReact.memoo un efecto/memo de dependencia realmente compara identidad, así que no lo rocíes en cada manejador. - Prefiere dispatch Estable Sobre useCallback: El
dispatchdeuseReduceres permanentemente estable en los renders, así que pasarlo hacia abajo es a menudo más limpio que envolver manejadores enuseCallbacken cada nivel; lo mismo aplica a los setters de estado. - Memoiza el Valor de Context: Pasar un objeto literal fresco como
<Context value={{…}}>re-renderiza cada consumidor en cada render del padre; envuelve el valor enuseMemo—<Ctx value={useMemo(() => ({ theme, toggle }), [theme, toggle])}>— o divide el estado y el actualizador en contextos separados para detener la avalancha. - El Contexto Por Defecto No Es Estado Inicial: El argumento a
createContext(default)se usa solo cuando ningún Provider envuelve el árbol, no como el valor inicial del Provider; confía en esto para protegerte contra proveedores faltantes con un hook personalizado que lance si el contexto es null. - Empareja useDeferredValue Con React.memo:
useDeferredValueaplaza el renderizado de consumidores, así que el hijo pesado debe estar envuelto enReact.memoo el padre lo re-renderiza con el valor actual de todas formas, haciendo que el hook sea un no-op. - Strict Mode Ejecuta Efectos Dos Veces: En desarrollo, Strict Mode monta → desmonta → remonta cada componente para exponer limpieza faltante; trátalo como una característica y haz cada efecto idempotente con un retorno de limpieza apropiado.
- Cancela Búsquedas Propensas a Carreras: Los cambios rápidos de dependencias pueden entregar respuestas más antiguas después de las más nuevas, así que empareja cada efecto de búsqueda con un
AbortControllery aborta en la limpieza para descartar resultados obsoletos:useEffect(() => { const ac = new AbortController(); fetch(url, { signal: ac.signal }); return () => ac.abort(); }, [url]). - No Pases Funciones Async a useEffect: Una función async devuelve una Promise, no una limpieza, así que define una función async interna y llámala:
useEffect(() => { async function load() { await fetch(url) } load(); }, [url])— React advierte sobre este patrón porque la Promise devuelta nunca se espera para la limpieza. - Usa useId Solo para IDs de Accesibilidad:
useIddevuelve un identificador estable SSR por posición de árbol — perfecto parahtmlFor,aria-describedbyy cableado de formularios — pero nunca parakeys de lista (un ID por llamada) y nunca dentro de bucles o condicionales. - Establece identifierPrefix para Múltiples Raíces: Dos raíces React en la misma página generan valores
useIdcolisionantes por defecto; pasaidentifierPrefixa cadacreateRoot/hydrateRootpara que micro-frontends o widgets incrustados se mantengan distintos. - useMemo No Es una Garantía: React puede descartar entradas de memoización (por ejemplo en componentes fuera de pantalla) y recomputar después, así que nunca confíes en
useMemopara corrección — úsalo para rendimiento y estabilidad de referencia, no semántica de programa. - Sin Efectos Secundarios en useMemo: La fábrica se ejecuta durante el renderizado, así que debe ser pura — logging, búsqueda o escritura en
localStoragedentro deuseMemorompe el modelo de renderizado de React y se comporta mal bajo Strict Mode y renderizado concurrente. - Llama addOptimistic Dentro de una Transición: La actualización de
useOptimisticdesaparece silenciosamente a menos queaddOptimisticse ejecute dentro de una acción de formulario, manejador de Server Action ostartTransitionexplícito; la superposición solo persiste mientras una transición está pendiente, con reversión automática en error. - Mantén los Reducers Puros: El reducer se ejecuta durante el renderizado, así que nunca mutes el estado —
return { ...state, count: state.count + 1 }nostate.count++; return state— nunca busques ni escribas en almacenamiento, e incluye siempre una ramadefault: return stateo acciones desconocidas limpian el estado aundefined. - Inicializa Perezosamente el Estado Costoso: Escribe
useState(() => computeInitial())yuseReducer(reducer, arg, init)para que el inicializador se ejecute solo al montar;useState(computeInitial())ejecuta el cálculo en cada renderizado y desecha el resultado. - Las Lecturas de Ref.current Durante el Renderizado No Son Seguras: Bajo renderizado concurrente,
.currentpuede reflejar un paso de renderizado diferente del que se pinta, así que lee refs dentro de efectos, manejadores de eventos o layout effects — no inline en el cuerpo del componente. - Los Refs del DOM Son null Hasta el Commit: Un
useRefadjunto a un nodo del DOM esnulldurante el primer renderizado, así que intentar tocarref.current.focus()inline lanza un error; hazlo enuseEffect,useLayoutEffecto un manejador, y recuerda que el ref de un elemento renderizado condicionalmente es null mientras está oculto. - Prefiere setState(prev => …): Tres llamadas directas a
setCount(count + 1)en el mismo manejador se incrementan solo una vez porque todas se cierran sobre el mismocountobsoleto; la forma del actualizadorsetCount(prev => prev + 1)se compone correctamente bajo el batching automático de React 18+. - Lazy useState Necesita una Callback:
useState(() => parseJSON(localStorage.x))ejecuta el análisis solo al montar;useState(parseJSON(localStorage.x))lo ejecuta en cada renderizado y descarta todo excepto el primero — que es costoso y puede lanzar durante SSR. - Solo el React State Es Transicionable:
startTransitionsolo aplaza setters de React state — refs, almacenes externos y escrituras DOM síncronas no se ven afectadas; también recuerda que React 18 requiere una callback síncrona mientras React 19 permite callbacksasyncnativamente. - Dale a use() una Promise Estable:
use(promise)trata un objeto promise fresco como una nueva solicitud pendiente, así que crear una promise inline en el renderizado causa suspensión infinita; la promise debe venir de props, un padre, un caché o un ref estable — y siempre sentarse bajo una Suspense más límite de error.