Volver a React Intermedio
Patrones Avanzados de React
Estos patrones resuelven problemas comunes de composición y reutilización de forma elegante.
Componentes Compuestos
Componentes que comparten estado implícito a través de Context:
interface ContextoPestañas {
pestañaActiva: string;
setPestañaActiva: (pestaña: string) => void;
}
const ContextoPestañas = createContext<ContextoPestañas | null>(null);
function Pestañas({ defaultPestaña, children }: { defaultPestaña: string; children: ReactNode }) {
const [pestañaActiva, setPestañaActiva] = useState(defaultPestaña);
return (
<ContextoPestañas.Provider value={{ pestañaActiva, setPestañaActiva }}>
<div className="pestañas">{children}</div>
</ContextoPestañas.Provider>
);
}
function ListaPestañas({ children }: { children: ReactNode }) {
return <div className="lista-pestañas" role="tablist">{children}</div>;
}
function Pestaña({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(ContextoPestañas)!;
return (
<button
role="tab"
aria-selected={ctx.pestañaActiva === id}
onClick={() => ctx.setPestañaActiva(id)}
className={ctx.pestañaActiva === id ? 'activa' : ''}
>
{children}
</button>
);
}
function PanelPestaña({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(ContextoPestañas)!;
if (ctx.pestañaActiva !== id) return null;
return <div role="tabpanel">{children}</div>;
}
Pestañas.Lista = ListaPestañas;
Pestañas.Pestaña = Pestaña;
Pestañas.Panel = PanelPestaña;
// Uso
function App() {
return (
<Pestañas defaultPestaña="perfil">
<Pestañas.Lista>
<Pestañas.Pestaña id="perfil">Perfil</Pestañas.Pestaña>
<Pestañas.Pestaña id="config">Configuración</Pestañas.Pestaña>
</Pestañas.Lista>
<Pestañas.Panel id="perfil"><Perfil /></Pestañas.Panel>
<Pestañas.Panel id="config"><Configuracion /></Pestañas.Panel>
</Pestañas>
);
}
Render Props
Pasar una función como prop para compartir lógica:
interface PosMouse { x: number; y: number; }
function RastreadorRaton({ render }: { render: (pos: PosMouse) => ReactNode }) {
const [pos, setPos] = useState<PosMouse>({ x: 0, y: 0 });
return (
<div
className="rastreador"
onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}
>
{render(pos)}
</div>
);
}
// Uso
<RastreadorRaton
render={({ x, y }) => (
<div>Ratón: {x}, {y}</div>
)}
/>
Higher-Order Components (HOC)
function conAutenticacion<P extends object>(Componente: React.ComponentType<P>) {
return function ComponenteAutenticado(props: P) {
const { usuario, cargando } = useAuth();
if (cargando) return <Spinner />;
if (!usuario) return <Navigate to="/login" />;
return <Componente {...props} />;
};
}
// Uso
const DashboardProtegido = conAutenticacion(Dashboard);
Máquinas de Estado con useReducer
type Estado = 'inactivo' | 'cargando' | 'exitoso' | 'error';
interface EstadoFetch<T> {
estado: Estado;
datos: T | null;
error: string | null;
}
type AccionFetch<T> =
| { type: 'FETCH' }
| { type: 'EXITO'; datos: T }
| { type: 'ERROR'; error: string };
function reductorFetch<T>(
state: EstadoFetch<T>,
action: AccionFetch<T>
): EstadoFetch<T> {
switch (action.type) {
case 'FETCH': return { estado: 'cargando', datos: null, error: null };
case 'EXITO': return { estado: 'exitoso', datos: action.datos, error: null };
case 'ERROR': return { estado: 'error', datos: null, error: action.error };
default: return state;
}
}