skill · sumi-react · v0.6.1
Componentes de React (React 19)
Diseño de componentes en React 19: composición en lugar de configuración, reglas de los hooks, cuándo NO usar useEffect, useActionState/useOptimistic/useFormStatus/use(), ref como prop, implicaciones del React Compiler (sin abusar de memo manual), colocación del estado, formularios controlados frente a no controlados, primitivas headless accesibles (Radix, React Aria) frente a widgets hechos a mano, y props tipadas. Úsala al escribir o revisar componentes *.tsx/*.jsx, hooks propios (use*.ts), formularios, proveedores de contexto, APIs de componentes o primitivas del sistema de diseño en components/**, o al refactorizar componentes grandes o código lleno de efectos.
- Referencia
- sumi-react
- skill
- react-components
- source
- plugins/sumi-react/skills/react-components/SKILL.md
Objetivo: React 19.2, TypeScript estricto, React Compiler activado siempre que sea posible. Estilos con Tailwind y los tokens de DESIGN.md (sumi:css-architecture). Tabla de decisión de efectos y refactorizaciones: references/effects.md.
Principios
- Un componente hace un solo trabajo. Si necesitas una "y" para describirlo, divídelo. Unas 150 líneas son un motivo de revisión, no un objetivo.
- Compón, no configures. Prefiere
childreny ranuras a la explosión de props booleanas o de configuración. - Deriva, no sincronices. Todo lo que se pueda calcular a partir de props o estado se calcula durante el renderizado.
- El estado vive lo más abajo posible y lo más arriba necesario. Súbelo solo hasta el propietario común más cercano; prefiere el estado en la URL para el estado de interfaz que se pueda compartir (filtros, pestañas, paginación).
- La semántica primero. Elementos nativos antes que ARIA; primitivas headless probadas antes que widgets hechos a mano (
sumi:a11y). - Deja que el compilador optimice. Escribe código llano; mide antes de memoizar.
Composición en lugar de configuración
ASÍ NO:
<Card title="Plan" subtitle="Pro" showIcon iconPosition="left" footerButtons={[...]} variant="highlight" />
ASÍ SÍ:
<Card tone="highlight">
<Card.Header>
<Card.Title>Plan</Card.Title>
<Card.Description>Pro</Card.Description>
</Card.Header>
<Card.Footer><Button>Upgrade</Button></Card.Footer>
</Card>
- Las variantes mediante un mapa tipado (
cvao un simpleRecord<Variant, string>), no ternarios improvisados enclassName. Combínalas concn()(clsx + tailwind-merge). - Acepta
classNamey propaga el resto de props nativas al elemento raíz para que quien consuma el componente pueda extenderlo. - Tipa las props a partir del elemento:
ComponentProps<'button'> & { tone?: Tone }. Sinany, sinReact.FC(sumi:js-ts). - Polimorfismo: prefiere una ranura
asChild(Slotde Radix) a las props genéricasascon tipado complejo.
Refs (React 19)
ref es una prop corriente en los componentes de función. No uses forwardRef en código nuevo.
export function Input({ ref, className, ...props }: ComponentProps<'input'>) {
return <input ref={ref} className={cn('input', className)} {...props} />;
}
Los callbacks de ref pueden devolver una función de limpieza. Usa refs para acceder al DOM y como vías de escape imperativas, no para estado que se renderiza.
Efectos: probablemente no necesitas uno
useEffect sirve para sincronizarse con un sistema externo (APIs del DOM, suscripciones, widgets que no son de React, conexiones de red). No sirve para:
| En lugar de un efecto para... | Haz esto |
|---|---|
| Derivar estado de props o estado | Calcúlalo en el renderizado (const total = items.reduce(...)) |
| Reaccionar a una acción del usuario | Pon la lógica en el manejador de eventos |
| Reiniciar el estado cuando cambia una prop | key={id} en el componente |
| Obtener los datos iniciales | Server Component, o use(promise); TanStack Query para datos dirigidos por el cliente |
| Avisar al padre de un cambio | Llama al callback del padre en el mismo manejador |
| Suscribirse a un almacén externo | useSyncExternalStore |
| Leer las últimas props dentro de un efecto sin volver a suscribirse | useEffectEvent (19.2) |
ASÍ NO:
const [fullName, setFullName] = useState('');
useEffect(() => setFullName(`${first} ${last}`), [first, last]);
ASÍ SÍ:
const fullName = `${first} ${last}`;
Todo efecto que quede: lista de dependencias correcta (sin suprimir el lint), limpieza para suscripciones, temporizadores y oyentes, e idempotente ante la doble invocación del Strict Mode.
Reglas de los hooks
- Llama a los hooks en el nivel superior, sin condiciones. Excepción:
use()puede llamarse de forma condicional. - Los hooks propios encapsulan un comportamiento reutilizable (
useMediaQuery,useDebouncedValue), no "sacar código del componente". Un hook que solo envuelve unuseStatees ruido. - Nombra los hooks
useX; devuelve tuplas para 2 valores y objetos a partir de ahí.
Formularios y acciones
- Por defecto, inputs no controlados +
<form action={fn}>+FormData. Funciona antes de la hidratación, sin estado por cada pulsación de tecla. - Inputs controlados solo cuando la interfaz reacciona a cada pulsación (formato en vivo, campos dependientes, búsqueda instantánea).
useActionState(action, initial)->[state, formAction, isPending]para resultados y errores.useFormStatus()dentro de un hijo del formulario para la interfaz de pendiente.useOptimisticpara respuesta instantánea; el resultado del servidor la reconcilia.- Formularios grandes del lado del cliente con validación compleja: React Hook Form + el resolver de zod es aceptable; aun así, envía mediante una Server Action.
- Todo input tiene un
<label>visible; los errores se vinculan conaria-describedby;aria-invalidal fallar; los mensajes de estado van en una región viva educada. Ejemplo completo: la skillnext-data-caching,references/server-actions.md(en este plugin:${CLAUDE_PLUGIN_ROOT}/skills/next-data-caching/references/server-actions.md).
use() y Suspense
use(promise)desenvuelve una promesa creada en el servidor (pasada como prop) o una cacheada. Nunca crees la promesa durante el renderizado del cliente: sería nueva en cada renderizado.use(Context)reemplaza auseContexty puede ser condicional.- Combínalo con
<Suspense>y una frontera de error en un nivel que tenga sentido.
React Compiler
- Con el compilador activado (
reactCompiler: trueennext.config; verifica dónde va el indicador en tu versión), los componentes y los hooks se memoizan automáticamente. - No añadas
useMemo,useCallbacknimemopor reflejo. Consérvalos solo donde el profiler demuestre un coste, o donde la identidad referencial sea un contrato (p. ej. las dependencias de efecto de un hook de terceros); comenta el porqué. - El compilador exige las Reglas de React: no mutar props ni estado, no leer refs durante el renderizado, renderizado puro. Ejecuta
eslint-plugin-react-hooks(la configuración recomendada incluye reglas del compilador) y corrige, no suprimas. 'use no memo'excluye un componente temporalmente; deja un TODO con el motivo.
Estado y contexto
- El estado del servidor no es estado del cliente: los datos del servidor permanecen en Server Components o en una caché de consultas, no se copian en
useState. - El contexto es para valores de baja frecuencia y necesarios en muchos sitios (tema, sesión, idioma). Divide los contextos por frecuencia de actualización. No uses un único contexto global como almacén.
- Pasar props por 2 niveles está bien. Más allá: composición (pasa el elemento ya renderizado como
children) antes que contexto; un almacén (Zustand) solo para estado de cliente realmente compartido y de alta frecuencia. useReducercuando las transiciones de estado están relacionadas y tienen nombre; una unión discriminada para el estado (idle | loading | success | error) en lugar de varios booleanos.
Primitivas accesibles
- Usa Radix UI, React Aria Components o Base UI para dialog, popover, menu, combobox, tabs, tooltip, select, slider. Dales estilo con tokens.
- Construye a mano solo widgets triviales (un desplegable mediante
<details>/botón +aria-expanded). Nunca undivcononClick: usa<button>; los enlaces navegan, los botones actúan. - Verifica: trampa y retorno del foco en los diálogos, Escape cierra, foco itinerante en menús y pestañas, anillo de foco visible (2.4.7/2.4.11), tamaño del objetivo >= 24px (2.5.8). Consulta
sumi:a11y.
Listas y claves
- Las claves son IDs estables que vienen de los datos, nunca el índice del array en listas reordenables o filtrables, nunca
Math.random(). - Los estados vacío, de carga y de error forman parte del contrato del componente, no son ocurrencias tardías.
Señales de relleno de IA
useEffect+setStatepara derivar valores o "sincronizar" props en el estado.useMemo/useCallbackalrededor de cada valor y manejador;memo()en cada componente.forwardRefen código nuevo de React 19;React.FC<Props>;props: any.- Componentes de 400 líneas que mezclan obtención de datos, formato, composición y modales.
- Diez props booleanas (
isPrimary,isLarge,hasIcon...) en lugar de variantes o composición. - Botones hechos con
<div onClick>, placeholder en lugar de etiqueta, botones de icono sin nombre accesible. - Booleanos
isLoading,isError,isSuccessque pueden contradecirse entre sí. - Colores y espaciados escritos a mano en lugar de los tokens de
DESIGN.md. // eslint-disable-next-line react-hooks/exhaustive-deps.
Lista de verificación
- Ningún efecto que derive estado, gestione un evento u obtenga los datos iniciales.
- Ningún
useMemo/useCallback/memoinjustificado; lint limpio con las reglas de hooks y del compilador. - Props tipadas a partir de elementos nativos; sin
any;classNamey el resto de props propagados. - Widgets interactivos construidos sobre elementos nativos o una primitiva headless; teclado y lector de pantalla comprobados.
- Formularios: etiquetas, errores vinculados, estado de pendiente, estado en región viva; funcionan sin JS cuando sea viable.
- Estado colocado cerca de su uso; estado en la URL para la interfaz compartible; sin estado del servidor copiado.
- Los estilos usan tokens; sin valores mágicos.
Efectos: guía de decisión y refactorizaciones
Pregunta en orden. Detente en el primer sí.
- ¿Se puede calcular a partir de las props o el estado actuales? -> calcúlalo durante el renderizado.
- ¿Lo causa una interacción concreta del usuario? -> manejador de eventos.
- ¿Debe reiniciarse el estado cuando cambia una identidad? ->
key. - ¿Son datos para renderizar? -> Server Component /
use()/ biblioteca de consultas. - ¿Es una suscripción a algo ajeno a React que cambia con el tiempo? ->
useSyncExternalStore. - ¿Es una sincronización con un sistema externo mientras el componente siga montado? ->
useEffectcon limpieza.
Refactorización 1: filtrado derivado
// Before
const [visible, setVisible] = useState<Todo[]>([]);
useEffect(() => { setVisible(todos.filter(t => t.status === filter)); }, [todos, filter]);
// After (compiler memoizes if needed)
const visible = todos.filter(t => t.status === filter);
Refactorización 2: lógica disparada por una acción
// Before: effect watches state set by a click
useEffect(() => { if (submitted) { toast('Saved'); track('save'); } }, [submitted]);
// After
async function handleSave() {
await save();
toast('Saved');
track('save');
}
Refactorización 3: reinicio al cambiar la identidad
// Before
useEffect(() => { setDraft(''); }, [threadId]);
// After: parent remounts the editor per thread
<CommentEditor key={threadId} threadId={threadId} />
Refactorización 4: ajustar el estado durante el renderizado (poco frecuente)
Cuando una parte del estado debe responder a un cambio de prop y key es demasiado grueso, guarda la prop anterior y ajusta durante el renderizado. Prefiere reestructurar primero.
const [prevItems, setPrevItems] = useState(items);
const [selection, setSelection] = useState<string | null>(null);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
Refactorización 5: almacén externo
function useOnlineStatus() {
return useSyncExternalStore(
(cb) => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine,
() => true, // server snapshot
);
}
Refactorización 6: el último valor en un efecto sin volver a suscribirse (React 19.2)
const onMessage = useEffectEvent((msg: Message) => {
if (!muted) notify(msg); // always reads current `muted`
});
useEffect(() => {
const conn = connect(roomId);
conn.on('message', (msg) => onMessage(msg));
return () => conn.disconnect();
}, [roomId]); // muted not a dependency
Verifica que useEffectEvent se exporte como estable en la versión de React instalada; no lo llames fuera de los efectos.
Efectos legítimos (consérvalos, con limpieza)
- Oyentes de
IntersectionObserver,ResizeObserverymatchMedia(ouseSyncExternalStore). - Integrar widgets que no son de React (mapas, gráficos, reproductores de vídeo): créalos en el efecto y destrúyelos en la limpieza.
- Suscripciones
onSnapshotde Firestore en hojas de cliente: devuelve la función de cancelación. - Atajos de teclado a nivel de documento.
- Gestión del foco tras un cambio de estado que no está ligado a un único manejador (prefiere antes el manejador o un callback de ref).
Strict Mode
En desarrollo se invocan los efectos dos veces (montaje -> limpieza -> montaje). Si eso rompe el comportamiento, la limpieza es incorrecta; no desactives el Strict Mode. Para una inicialización única de la aplicación, usa una guarda a nivel de módulo, no un efecto.