Diseña APIs de componentes flexibles con los compound components, el state reducer y la elección correcta entre render props y hooks.
Abrir esta lección en KodokonUn componente <Tabs items={...} labels={...} icons={...} /> siempre acaba sepultado bajo props de configuración. El patrón de compound components invierte el enfoque: varios componentes colaboran y comparten estado implícito a través de un contexto, mientras el usuario mantiene el control de la composición JSX (orden, anidamiento, elementos intercalados). Es el modelo de <select> y <option> en HTML, y el que usan bibliotecas como Radix o Headless UI.
const TabsContext = createContext(null);
function Tabs({ defaultTab, children }) {
const [active, setActive] = useState(defaultTab);
const value = useMemo(
() => ({ active, setActive }),
[active]
);
return (
<TabsContext.Provider value={value}>
{children}
</TabsContext.Provider>
);
}Cada subcomponente lee el contexto y no necesita ninguna prop de conexión: el usuario compone libremente, y el patrón garantiza la coherencia. Fíjate en el valor del contexto envuelto en useMemo: sin él, cada renderizado del padre crearía un objeto nuevo y forzaría un renderizado de todos los consumidores.
function Tab({ id, children }) {
const ctx = useContext(TabsContext);
return (
<button
aria-selected={ctx.active === id}
onClick={() => ctx.setActive(id)}
>
{children}
</button>
);
}
// <Tabs defaultTab="a">
// <Tab id="a">Profile</Tab>
// <Tab id="b">Settings</Tab>
// </Tabs>Segundo patrón: la inversión de control, cuya forma más refinada es el state reducer popularizado por Kent C. Dodds en Downshift. En lugar de añadir una prop booleana por cada nueva necesidad, el hook expone sus transiciones de estado: quien llama proporciona un reducer que recibe cada acción y puede dejarla pasar, modificarla o cancelarla. La biblioteca conserva su comportamiento por defecto; el control fino pertenece al consumidor.
function defaultReducer(state, action) {
if (action.type === 'toggle') {
return { on: !state.on };
}
return state;
}
function useToggle({ reducer = defaultReducer } = {}) {
const [state, dispatch] = useReducer(reducer, {
on: false,
});
const toggle = () => dispatch({ type: 'toggle' });
return { on: state.on, toggle };
}Por último, el eterno debate de render props frente a hooks. Los hooks han ganado para compartir lógica: sin falsa jerarquía en el árbol, sin pirámide de wrappers, con composición natural de varias fuentes. Pero el render prop conserva un territorio legítimo: cuando la variación concierne al propio renderizado. Una lista virtualizada que te pregunta cómo dibujar cada fila, un componente headless que delega todo su JSX: ahí, una función de renderizado pasada como prop sigue siendo la herramienta más directa, porque un hook no puede decidir dónde insertar el JSX en el árbol de quien llama.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const onMove = (e) =>
setPos({ x: e.clientX, y: e.clientY });
return (
<div onMouseMove={onMove}>{render(pos)}</div>
);
}
// For the logic alone, a hook is enough:
// const pos = useMousePosition();