//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Crie componentes e hooks React reutilizáveis e type-safe usando generics do TypeScript. Crie componentes que funcionam com qualquer tipo de dado, preservando a informação completa de tipo para os 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 - O TypeScript infere 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 é tipado como User
renderItem={(user) => <span>{user.name}</span>} // user é tipado como User
/>// Hook customizado 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 é inferido como { theme: string; fontSize: number }
const [settings, setSettings] = useLocalStorage("settings", {
theme: "dark",
fontSize: 14,
});<T>) que atua como um placeholder. Quando o componente é usado, o TypeScript infere T a partir das props que você passa.function List<T>({ items }: ListProps<T>) declara um componente de função genérico. O <T> deve vir antes da lista de parâmetros.<List items={users} ...>, ele vê que items é User[], então T se torna User.Generics com restrições (exigindo uma 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 deve ter um campo id
<Table data={users} columns={["name", "email"]} />Componente select genérico:
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últiplos parâmetros de tipo:
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> em funções de seta pode ser confundido com JSX. Use <T,> (vírgula final) ou <T extends unknown> para desambiguar: const List = <T,>({ items }: ListProps<T>) => ....extends em generics atua como uma restrição, não como herança. T extends HasId significa "T deve ter pelo menos as propriedades de HasId."keyof T fornece uma união de todos os nomes de propriedades de T. Combinado com generics, permite acesso a propriedades type-safe.React.FC com componentes genéricos. Escreva-os como declarações de função regulares: function List<T>(props: ListProps<T>)..tsx precisam do truque da vírgula final: <T,> em vez de <T>. Sem isso, o parser pensa que <T> é uma tag JSX.<List<User> items={users} ...>.| Abordagem | Prós | Contras |
|---|---|---|
| Componentes genéricos | Inferência de tipo completa, reutilizável | Assinaturas mais complexas |
| Tipos de prop de união | Mais simples de ler | Deve enumerar todos os tipos suportados |
any props | Flexibilidade máxima | Sem segurança de tipo |
| Render props com generics | Componível, type-safe | Aninhamento verboso |
| HOC com generics | Wrapper de lógica reutilizável | Encaminhamento de tipo complexo |
De uma aplicação SaaS de produção Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: Hook genérico de streaming SSE para múltiplos tipos de ferramentas
// Arquivo: 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;
}
// Análise de seção específica da ferramenta via injeção de callback
const parsed = onSectionReceived(event, data);
if (parsed) {
setStreamState((prev) => ({
...prev,
sections: { ...prev.sections, ...parsed } as TSections,
streamingSection: event,
}));
}
} catch { /* ignora erros de análise de evento individual */ }
},
[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 }),
});
// ... análise de stream SSE com handleSseEvent
}, [toolId, toolSlug, handleSseEvent]);
const canGenerate = !streamState.isStreaming && streamState.generationCount < maxGenerations;
return { streamState, generate, abort: () => abortControllerRef.current?.abort(), canGenerate };
}O que isso demonstra em produção:
TSections permite que cada ferramenta defina sua própria forma de seção enquanto compartilha a lógica de streaming.onSectionReceived é o ponto de injeção. Cada ferramenta fornece seu próprio parser, mas o streaming, o gerenciamento de estado, o tratamento de abort e a lógica CSRF são compartilhados.BuilderStreamState<TSections> carrega o genérico através do tipo de estado para que streamState.sections seja devidamente tipado por ferramenta.abortControllerRef permite o cancelamento de requisições em andamento. Requisições anteriores são abortadas quando uma nova geração começa.maxGenerations limita quantas vezes um usuário pode gerar por sessão (controle de custo).T a partir da primeira prop que usa o parâmetro de tipo.<List items={users}>, ele vê que items é User[], então T se torna User.T são então tipadas como User automaticamente.React.FC<Props> não suporta um parâmetro de tipo no próprio componente.function List<T>(props: ListProps<T>).React.FC.// Sem vírgula final: o parser pensa que <T> é uma tag JSX
// const List = <T>(props: ListProps<T>) => ... // ERRO
// Com vírgula final: desambigua do JSX
const List = <T,>(props: ListProps<T>) => { ... };
// Alternativa: use extends
const List = <T extends unknown>(props: ListProps<T>) => { ... };T extends HasId significa "T deve ter pelo menos as propriedades de HasId."HasId em valores do 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 flui através do tipo de retorno.keyof T fornece uma união de todos os nomes de propriedades de T.columns: (keyof T)[] restringe os valores das colunas às propriedades reais de T.<List<User> items={users} ...>.<T> sem restrição aceita qualquer tipo, maximizando a flexibilidade.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, o tipo de retorno de [value, setValue] é inferido como (T | SetStateAction<T>)[].as const, ele se torna uma tupla readonly [T, Dispatch<SetStateAction<T>>].Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥