Componente Genérico React com TypeScript
Componentes genericos sao o superpoder do TypeScript no React. Aprenda a criar listas, selects, formularios e tables que funcionam com qualquer tipo de dado.
Por que isso é importante
Componente Genérico React com TypeScript. Componentes genericos sao o superpoder do TypeScript no React. Aprenda a criar listas, selects, formularios e tables que funcionam com qualquer tipo de dado.
O que e um componente generico no React
Um componente generico e uma funcao React que recebe um tipo como parametro alem das props normais. Em vez de tipar os dados como 'any' ou criar N versoes do mesmo componente, voce usa um generico T que o TypeScript resolve automaticamente baseado nos dados que voce passa.
Pensa assim: quando voce faz useState<string>(''), o useState e generico e voce passa string como tipo. Componentes genericos funcionam do mesmo jeito, so que voce define o generico na hora de usar o componente.
// Componente NAO generico (restrito a um tipo)
interface UserListProps {
items: User[];
renderItem: (item: User) => ReactNode;
}
interface ProductListProps {
items: Product[];
renderItem: (item: Product) => ReactNode;
}
// Codigo duplicado! Mesma estrutura, tipos diferentes.
// Componente generico (funciona com qualquer tipo)
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}
// Um unico componente pra todos os casos.
Passo a passo: criando componente generico
Exemplos praticos de componentes genericos
Lista generica reutilizavel
O exemplo classico: uma lista que renderiza qualquer tipo de item. Voce passa os dados e uma funcao de render, e o TypeScript garante que tudo bate.
import { ReactNode } from 'react';
// Props genericas com constraint
interface ListProps<T extends { id: string | number }> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
emptyMessage?: string;
loading?: boolean;
className?: string;
}
// Componente generico
function List<T extends { id: string | number }>({
items,
renderItem,
emptyMessage = 'Nenhum item encontrado',
loading = false,
className,
}: ListProps<T>) {
if (loading) return <div>Carregando...</div>;
if (items.length === 0) {
return <p className="text-gray-500">{emptyMessage}</p>;
}
return (
<ul className={className}>
{items.map((item, index) => (
<li key={item.id}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Uso com User — TypeScript infere T = User
interface User {
id: string;
name: string;
email: string;
}
const users: User[] = [
{ id: '1', name: 'Ana', email: 'ana@mail.com' },
{ id: '2', name: 'Carlos', email: 'carlos@mail.com' },
];
<List
items={users}
renderItem={(user) => (
<div>
<strong>{user.name}</strong> {/* autocomplete funciona! */}
<span>{user.email}</span>
</div>
)}
/>
// Uso com Product — mesmo componente, tipo diferente
interface Product {
id: number;
name: string;
price: number;
}
<List
items={products}
renderItem={(product) => (
<div>
{product.name} - R${product.price} {/* tipado! */}
</div>
)}
/>
Select generico
Outro componente que todo projeto precisa: um select que funciona com qualquer tipo de opcao. O generico garante que o onChange retorna o tipo certo.
import { ReactNode } from 'react';
interface SelectProps<T> {
options: T[];
value: T | null;
onChange: (selected: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
placeholder?: string;
}
function Select<T>({
options,
value,
onChange,
getLabel,
getValue,
placeholder = 'Selecione...',
}: SelectProps<T>) {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = options.find(
(opt) => getValue(opt) === e.target.value
);
if (selected) onChange(selected);
};
return (
<select
value={value ? getValue(value) : ''}
onChange={handleChange}
>
<option value="">{placeholder}</option>
{options.map((opt) => (
<option key={getValue(opt)} value={getValue(opt)}>
{getLabel(opt)}
</option>
))}
</select>
);
}
// Uso: TypeScript sabe que selected e do tipo Country
interface Country {
code: string;
name: string;
population: number;
}
const [country, setCountry] = useState<Country | null>(null);
<Select
options={countries}
value={country}
onChange={setCountry} // (selected: Country) => void
getLabel={(c) => c.name} // TypeScript sabe que c e Country
getValue={(c) => c.code}
/>
Table generica com colunas tipadas
Uma table generica e mais sofisticada. Voce define as colunas com accessor tipado, e o TypeScript garante que a coluna referencia um campo real do tipo de dado.
import { ReactNode } from 'react';
// Coluna tipada com keyof
interface Column<T> {
header: string;
accessor: keyof T;
render?: (value: T[keyof T], row: T) => ReactNode;
}
interface TableProps<T extends { id: string | number }> {
data: T[];
columns: Column<T>[];
onRowClick?: (row: T) => void;
}
function Table<T extends { id: string | number }>({
data,
columns,
onRowClick,
}: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.accessor)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr
key={row.id}
onClick={() => onRowClick?.(row)}
style={{ cursor: onRowClick ? 'pointer' : 'default' }}
>
{columns.map((col) => (
<td key={String(col.accessor)}>
{col.render
? col.render(row[col.accessor], row)
: String(row[col.accessor])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Uso: colunas tipadas com base no tipo Employee
interface Employee {
id: number;
name: string;
role: string;
salary: number;
}
const columns: Column<Employee>[] = [
{ header: 'Nome', accessor: 'name' },
{ header: 'Cargo', accessor: 'role' },
{
header: 'Salario',
accessor: 'salary',
render: (value) => `R$ ${Number(value).toFixed(2)}`,
},
// { header: 'Teste', accessor: 'inexistente' } // ERRO!
];
<Table
data={employees}
columns={columns}
onRowClick={(emp) => console.log(emp.name)}
/>
forwardRef com generics
Esse e o caso mais avancado. O forwardRef do React nao suporta generics diretamente, entao voce precisa de um workaround com type assertion. Olha como fica:
import { forwardRef, Ref, ReactNode } from 'react';
// Props genericas do input
interface GenericInputProps<T> {
value: T;
onChange: (value: T) => void;
parse: (raw: string) => T;
format: (value: T) => string;
label: string;
}
// Workaround: componente interno generico
function GenericInputInner<T>(
{
value,
onChange,
parse,
format,
label,
}: GenericInputProps<T>,
ref: Ref<HTMLInputElement>
) {
return (
<label>
{label}
<input
ref={ref}
value={format(value)}
onChange={(e) => onChange(parse(e.target.value))}
/>
</label>
);
}
// Type assertion pra manter o generico
export const GenericInput = forwardRef(GenericInputInner) as <T>(
props: GenericInputProps<T> & { ref?: Ref<HTMLInputElement> }
) => ReactNode;
// Uso com numero
<GenericInput
value={42}
onChange={(n) => console.log(n)} // n e number
parse={Number}
format={String}
label="Quantidade"
/>
// Uso com Date
<GenericInput
value={new Date()}
onChange={(d) => console.log(d)} // d e Date
parse={(s) => new Date(s)}
format={(d) => d.toISOString().split('T')[0]}
label="Data"
/>
Erros comuns com componentes genericos
Armadilhas que travam a galera
O erro mais frequente e esquecer do constraint (extends) e tentar acessar propriedades do generico. Se T nao tem constraint, o TypeScript trata como unknown e voce nao consegue acessar nada. Outro problema: em arquivos .tsx, a sintaxe <T> de arrow functions confunde o parser com JSX. Use <T,> com virgula ou function declarations.
// ERRADO: acessar .id sem constraint
function List<T>({ items }: { items: T[] }) {
return items.map(item => (
<li key={item.id}>{/* ERRO: Property 'id' does not exist on type 'T' */}</li>
));
}
// CERTO: constraint garante que T tem id
function List<T extends { id: string | number }>({ items }: { items: T[] }) {
return items.map(item => (
<li key={item.id}>...</li> // OK!
));
}
// ERRADO em .tsx: parser confunde com JSX tag
const MyComponent = <T>(props: Props<T>) => { ... };
// Erro de syntax!
// CERTO: virgula desambigua o generico
const MyComponent = <T,>(props: Props<T>) => { ... };
// OU: use function declaration (mais limpo)
function MyComponent<T>(props: Props<T>) { ... }
// ERRADO: generico desnecessario
function Badge<T>({ text }: { text: string }) {
// T nunca e usado! Remove o generico.
return <span>{text}</span>;
}
Checklist: componente generico profissional
Checklist Final
Construa componentes profissionais no projeto real
Projeto completo com TypeScript
Componentes genericos sao o que separa projetos amadores de projetos profissionais em React. No CrazyStack voce constroi um sistema completo com tables, selects, formularios e listas genericas — tudo tipado e pronto pra producao com TypeScript, React e Node.js.