//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Construye componentes y hooks de React reutilizables y type-safe usando genéricos de TypeScript. Crea componentes que funcionan con cualquier tipo de datos mientras preservas información de tipo completa para los consumidores.
// Componente de lista genérico
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Uso - TypeScript infiere T como User
type User = { id: string; name: string; email: string };
const users: User[] = [
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
];
<List
items={users}
keyExtractor={(user) => user.id} // user está tipado como User
renderItem={(user) => <span>{user.name}</span>} // user está tipado como User
/>// Hook personalizado genérico
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
// Uso - T se infiere como { theme: string; fontSize: number }
const [settings, setSettings] = useLocalStorage("settings", {
theme: "dark",
fontSize: 14,
});<T>) que actúa como un placeholder. Cuando se usa el componente, TypeScript infiere T del props que pasas.function List<T>({ items }: ListProps<T>) declara una función componente genérica. El <T> debe venir antes de la lista de parámetros.<List items={users} ...>, ve que items es User[], así que T se convierte en User.Genéricos restringidos (requiriendo una forma):
type HasId = { id: string };
type TableProps<T extends HasId> = {
data: T[];
columns: (keyof T)[];
};
function Table<T extends HasId>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map((col) => <th key={String(col)}>{String(col)}</th>)}</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.id}>
{columns.map((col) => <td key={String(col)}>{String(row[col])}</td>)}
</tr>
))}
</tbody>
</table>
);
}
// Uso - T debe tener un field id
<Table data={users} columns={["name", "email"]} />Componente genérico select:
type SelectProps<T> = {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
};
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selected = options.find((opt) => getValue(opt) === e.target.value);
if (selected) onChange(selected);
}}
>
{options.map((opt) => (
<option key={getValue(opt)} value={getValue(opt)}>
{getLabel(opt)}
</option>
))}
</select>
);
}Múltiples type parameters:
function usePairedState<A, B>(initialA: A, initialB: B) {
const [a, setA] = useState<A>(initialA);
const [b, setB] = useState<B>(initialB);
return { a, setA, b, setB };
}.tsx, <T> en arrow functions puede confundirse con JSX. Usa <T,> (coma al final) o <T extends unknown> para desambiguar: const List = <T,>({ items }: ListProps<T>) => ....extends en genéricos actúa como una limitación, no herencia. T extends HasId significa "T debe tener al menos las propiedades de HasId."keyof T te da una unión de todos los nombres de propiedades de T. Combinado con genéricos, habilita acceso type-safe a propiedades.React.FC con componentes genéricos. Escríbelos como declaraciones de función regulares: function List<T>(props: ListProps<T>)..tsx necesitan el truco de la coma al final: <T,> en lugar de <T>. Sin ello, el parser piensa que <T> es una etiqueta JSX.<List<User> items={users} ...>.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Componentes genéricos | Inferencia de tipo completa, reutilizable | Firmas más complejas |
| Tipos de prop unión | Más simple de leer | Debes enumerar todos los tipos soportados |
Props any | Flexibilidad máxima | Sin type safety |
| Render props con genéricos | Composable, type-safe | Nesting verboso |
| HOC con genéricos | Lógica wrapper reutilizable | Forwarding de tipo complejo |
Desde una aplicación SaaS Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo en producción: Hook genérico de SSE streaming para múltiples tipos de herramientas
// Archivo: src/hooks/use-builder-generator.ts
export function useBuilderGenerator<
TSections = Record<string, unknown>,
>(options: UseBuilderGeneratorOptions) {
const { toolId, toolSlug, maxGenerations = 3, onSectionReceived } = options;
const [streamState, setStreamState] = useState<
BuilderStreamState<TSections>
>(INITIAL_STREAM_STATE as BuilderStreamState<TSections>);
const abortControllerRef = useRef<AbortController | null>(null);
const handleSseEvent = useCallback(
(event: string, data: string) => {
try {
if (event === 'error') {
setStreamState((prev) => ({ ...prev, error: JSON.parse(data).error || 'Generation failed.' }));
return;
}
if (event === 'done') {
setStreamState((prev) => ({ ...prev, streamingSection: null }));
return;
}
// Parsing de sección específica de herramienta vía inyección de callback
const parsed = onSectionReceived(event, data);
if (parsed) {
setStreamState((prev) => ({
...prev,
sections: { ...prev.sections, ...parsed } as TSections,
streamingSection: event,
}));
}
} catch { /* ignora errores de parseo de eventos individuales */ }
},
[onSectionReceived]
);
const generate = useCallback(async (inputs: Record<string, unknown>, model: string) => {
abortControllerRef.current?.abort();
const controller = new AbortController();
abortControllerRef.current = controller;
setStreamState({ ...(INITIAL_STREAM_STATE as BuilderStreamState<TSections>), isStreaming: true });
const csrfToken = await getCsrfToken();
const response = await fetch(`/api/tools/${toolSlug}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
signal: controller.signal,
body: JSON.stringify({ inputs, model }),
});
// ... parseo de stream SSE con handleSseEvent
}, [toolId, toolSlug, handleSseEvent]);
const canGenerate = !streamState.isStreaming && streamState.generationCount < maxGenerations;
return { streamState, generate, abort: () => abortControllerRef.current?.abort(), canGenerate };
}Lo que esto demuestra en producción:
TSections genérico permite que cada herramienta defina su propia forma de sección mientras comparte la lógica de streamingonSectionReceived es el punto de inyección. Cada herramienta proporciona su propio parser, pero el streaming, manejo de estado, manejo de abort, y lógica CSRF son compartidosBuilderStreamState<TSections> lleva el genérico a través del tipo de estado para que streamState.sections esté tipado correctamente por herramientaabortControllerRef habilita cancelación de requests en vuelo. Los requests previos se abortan cuando comienza una nueva generaciónmaxGenerations limita cuántas veces un usuario puede generar por sesión (control de costos)T del primer prop que usa el type parameter.<List items={users}>, ve que items es User[], así que T se convierte en User.T luego son tipados como User automáticamente.React.FC<Props> no soporta un type parameter en el componente mismo.function List<T>(props: ListProps<T>).React.FC.// Sin coma al final: parser piensa que <T> es una etiqueta JSX
// const List = <T>(props: ListProps<T>) => ... // ERROR
// Con coma al final: desambigua de JSX
const List = <T,>(props: ListProps<T>) => { ... };
// Alternativa: usa extends
const List = <T extends unknown>(props: ListProps<T>) => { ... };T extends HasId significa "T debe tener al menos las propiedades de HasId."HasId en valores de tipo T.function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
});
return [value, setValue] as const;
}T fluye a través del tipo de retorno.keyof T da una unión de todos los nombres de propiedades de T.columns: (keyof T)[] restringe valores de columnas a propiedades reales de T.<List<User> items={users} ...>.<T> sin limitación acepta cualquier tipo, maximizando flexibilidad.function usePairedState<A, B>(initialA: A, initialB: B) {
const [a, setA] = useState<A>(initialA);
const [b, setB] = useState<B>(initialB);
return { a, setA, b, setB };
}as const, el tipo de retorno de [value, setValue] se infiere como (T | SetStateAction<T>)[].as const, se convierte en una tuple readonly [T, Dispatch<SetStateAction<T>>].Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥