<h2>O que são Componentes Genéricos em React com TypeScript?</h2>
<p>Componentes genéricos são componentes React que aceitam tipos genéricos como parâmetros, permitindo que você crie componentes reutilizáveis e type-safe. Quando você trabalha com TypeScript em React, essa capacidade se torna extremamente poderosa porque você consegue definir componentes que funcionam com múltiplos tipos de dados mantendo segurança total em tempo de compilação.</p>
<p>Imagine que você precisa criar um componente de lista que funcione tanto para listas de usuários, produtos, comentários ou qualquer outra coisa. Sem genéricos, você teria que recriar esse componente várias vezes ou usar <code>any</code> para contornar o problema. Com genéricos, você escreve uma única implementação que funciona perfeitamente para todos os casos. A sintaxe é simples: você usa <code><T></code> (ou qualquer letra/nome) para representar um tipo que será definido quando o componente for utilizado.</p>
<h2>Fundamentos de Genéricos em TypeScript</h2>
<h3>Genéricos Básicos</h3>
<p>Antes de aplicar genéricos em componentes React, é importante entender como funcionam em TypeScript puro. Genéricos permitem que você escreva código que funcione com múltiplos tipos enquanto mantém a informação de tipo intacta. Você define um parâmetro de tipo entre colchetes angulares e o usa dentro da sua função ou classe.</p>
<pre><code class="language-typescript">// Função genérica simples
function identidade<T>(valor: T): T {
return valor;
}
const numeroIdentidade = identidade<number>(42); // tipo: number
const stringIdentidade = identidade<string>("olá"); // tipo: string
// O TypeScript infere o tipo automaticamente
const booleanoIdentidade = identidade(true); // tipo: boolean (inferido)</code></pre>
<p>O poder dos genéricos aparece quando você trabalha com estruturas de dados. Vamos considerar um exemplo prático onde criamos uma classe para gerenciar uma lista de qualquer tipo:</p>
<pre><code class="language-typescript">class Repositorio<T> {
private items: T[] = [];
adicionar(item: T): void {
this.items.push(item);
}
obter(indice: number): T {
return this.items[indice];
}
listar(): T[] {
return this.items;
}
}
// Usando o repositório para diferentes tipos
const repoUsuarios = new Repositorio<{ id: number; nome: string }>();
repoUsuarios.adicionar({ id: 1, nome: "João" });
const repoProdutos = new Repositorio<{ id: number; preco: number }>();
repoProdutos.adicionar({ id: 1, preco: 99.99 });</code></pre>
<h3>Restrições de Genéricos</h3>
<p>Nem sempre você quer aceitar qualquer tipo. TypeScript permite que você restrinja quais tipos podem ser passados como genéricos usando a palavra-chave <code>extends</code>. Isso é essencial para garantir que seu componente receba tipos que possuem as propriedades necessárias.</p>
<pre><code class="language-typescript">// Apenas tipos que têm a propriedade 'id'
interface Identificavel {
id: number;
}
class RepositorioSeguro<T extends Identificavel> {
private items: Map<number, T> = new Map();
salvar(item: T): void {
this.items.set(item.id, item);
}
obter(id: number): T | undefined {
return this.items.get(id);
}
}
// Isso funciona
const repo = new RepositorioSeguro<{ id: number; nome: string }>();
// Isso gera erro em tempo de compilação
// const repoInvalido = new RepositorioSeguro<{ nome: string }>();
// Erro: Type does not satisfy constraint Identificavel</code></pre>
<h2>Componentes Genéricos em React</h2>
<h3>Componentes Funcionais Genéricos</h3>
<p>A forma mais moderna de criar componentes genéricos em React é usando componentes funcionais com TypeScript. Você define o tipo genérico entre colchetes angulares e o usa tanto nas props quanto no retorno do componente.</p>
<pre><code class="language-typescript">import React, { ReactNode } from 'react';
// Componente genérico simples
interface ListaProps<T> {
items: T[];
renderItem: (item: T, indice: number) => ReactNode;
titulo?: string;
}
function Lista<T>({ items, renderItem, titulo }: ListaProps<T>) {
return (
<div className="lista">
{titulo && <h2>{titulo}</h2>}
<ul>
{items.map((item, indice) => (
<li key={indice}>{renderItem(item, indice)}</li>
))}
</ul>
</div>
);
}
export default Lista;</code></pre>
<p>Esse componente é extremamente flexível. Você pode usá-lo com qualquer tipo de dado desde que forneça uma função para renderizar cada item:</p>
<pre><code class="language-typescript">// Usando com usuários
interface Usuario {
id: number;
nome: string;
email: string;
}
const usuarios: Usuario[] = [
{ id: 1, nome: "Ana", email: "ana@email.com" },
{ id: 2, nome: "Bruno", email: "bruno@email.com" }
];
// Renderizar a lista de usuários
<Lista<Usuario>
items={usuarios}
titulo="Usuários do Sistema"
renderItem={(usuario) => ${usuario.nome} (${usuario.email})}
/>
// Usando com números
const numeros = [1, 2, 3, 4, 5];
<Lista<number>
items={numeros}
titulo="Números Importantes"
renderItem={(num) => Número: ${num}}
/></code></pre>
<h3>Componentes com Múltiplos Genéricos</h3>
<p>Às vezes você precisa de mais de um tipo genérico em um componente. Isso é totalmente válido e muito útil para cenários complexos:</p>
<pre><code class="language-typescript">import React from 'react';
interface PaginacaoProps<T, K> {
items: T[];
chaveId: K;
renderizar: (item: T) => React.ReactNode;
itensPorPagina?: number;
}
function ComponentePaginado<T extends Record<string, any>, K extends keyof T>({
items,
chaveId,
renderizar,
itensPorPagina = 5
}: PaginacaoProps<T, K>) {
const [paginaAtual, setPaginaAtual] = React.useState(0);
const inicio = paginaAtual * itensPorPagina;
const fim = inicio + itensPorPagina;
const itemsPaginados = items.slice(inicio, fim);
const totalPaginas = Math.ceil(items.length / itensPorPagina);
return (
<div className="paginado">
<div className="items">
{itemsPaginados.map((item) => (
<div key={String(item[chaveId])} className="item">
{renderizar(item)}
</div>
))}
</div>
<div className="paginacao">
<button
disabled={paginaAtual === 0}
onClick={() => setPaginaAtual(p => p - 1)}
>
Anterior
</button>
<span>
Página {paginaAtual + 1} de {totalPaginas}
</span>
<button
disabled={paginaAtual >= totalPaginas - 1}
onClick={() => setPaginaAtual(p => p + 1)}
>
Próxima
</button>
</div>
</div>
);
}
// Uso
interface Produto {
id: number;
nome: string;
preco: number;
}
const produtos: Produto[] = [
{ id: 1, nome: "Notebook", preco: 3000 },
{ id: 2, nome: "Mouse", preco: 50 },
{ id: 3, nome: "Teclado", preco: 150 },
// ... mais produtos
];
<ComponentePaginado<Produto, 'id'>
items={produtos}
chaveId="id"
renderizar={(produto) => (
<div>
<h4>{produto.nome}</h4>
<p>R$ {produto.preco.toFixed(2)}</p>
</div>
)}
itensPorPagina={3}
/></code></pre>
<h3>Componentes Genéricos com Contexto</h3>
<p>Para casos mais avançados, você pode combinar genéricos com Context API para criar soluções elegantes e reutilizáveis:</p>
<pre><code class="language-typescript">import React, { createContext, useContext, useState, ReactNode } from 'react';
// Criando um contexto genérico para gerenciar qualquer tipo de lista
interface ContextoLista<T> {
items: T[];
adicionar: (item: T) => void;
remover: (indice: number) => void;
atualizar: (indice: number, item: T) => void;
limpar: () => void;
}
// Factory function para criar um contexto genérico
function criarContextoLista<T>() {
const contexto = createContext<ContextoLista<T> | undefined>(undefined);
function ProvedorLista({ children }: { children: ReactNode }) {
const [items, setItems] = useState<T[]>([]);
const valor: ContextoLista<T> = {
items,
adicionar: (item) => setItems(prev => [...prev, item]),
remover: (indice) => setItems(prev => prev.filter((_, i) => i !== indice)),
atualizar: (indice, item) => {
setItems(prev => {
const nova = [...prev];
nova[indice] = item;
return nova;
});
},
limpar: () => setItems([])
};
return (
<contexto.Provider value={valor}>
{children}
</contexto.Provider>
);
}
function usarLista(): ContextoLista<T> {
const ctx = useContext(contexto);
if (!ctx) {
throw new Error('usarLista deve ser usado dentro de ProvedorLista');
}
return ctx;
}
return { ProvedorLista, usarLista };
}
// Criando um contexto específico para tarefas
interface Tarefa {
id: number;
descricao: string;
concluida: boolean;
}
const { ProvedorLista: ProvedorTarefas, usarLista: usarTarefas } =
criarContextoLista<Tarefa>();
// Componente que usa o contexto
function ListaTarefas() {
const { items, adicionar, remover } = usarTarefas();
const [descricao, setDescricao] = useState('');
const handleAdicionar = () => {
if (descricao.trim()) {
adicionar({
id: Date.now(),
descricao,
concluida: false
});
setDescricao('');
}
};
return (
<div>
<input
value={descricao}
onChange={(e) => setDescricao(e.target.value)}
placeholder="Nova tarefa..."
/>
<button onClick={handleAdicionar}>Adicionar</button>
<ul>
{items.map((tarefa, indice) => (
<li key={tarefa.id}>
{tarefa.descricao}
<button onClick={() => remover(indice)}>Remover</button>
</li>
))}
</ul>
</div>
);
}
// Uso
export default function App() {
return (
<ProvedorTarefas>
<ListaTarefas />
</ProvedorTarefas>
);
}</code></pre>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Genéricos com Constantes de Tipo</h3>
<p>TypeScript 3.4 introduziu <code>as const</code> que permite trabalhar com tipos literais. Isso é especialmente útil em componentes genéricos quando você quer manter informações específicas de tipo:</p>
<pre><code class="language-typescript">interface Opcao<T extends readonly string[]> {
valores: T;
selecionado?: T[number];
onChange?: (valor: T[number]) => void;
}
function Dropdown<const T extends readonly string[]>({
valores,
selecionado,
onChange
}: Opcao<T>) {
return (
<select value={selecionado} onChange={(e) => onChange?.(e.target.value as T[number])}>
{valores.map((valor) => (
<option key={valor} value={valor}>
{valor}
</option>
))}
</select>
);
}
// Tipo é inferido como ["ativo", "inativo", "pendente"]
const opcoes = ["ativo", "inativo", "pendente"] as const;
<Dropdown
valores={opcoes}
selecionado="ativo"
onChange={(valor) => {
// valor é inferido como "ativo" | "inativo" | "pendente"
console.log(valor);
}}
/></code></pre>
<h3>Condicionais de Tipo</h3>
<p>Para cenários mais complexos, você pode usar tipos condicionais para alterar o comportamento baseado no tipo genérico:</p>
<pre><code class="language-typescript">// Tipo condicional: se T é array, retorna o tipo do elemento, senão retorna T
type ElementoOuValor<T> = T extends (infer E)[] ? E : T;
interface Comparador<T> {
items: T[];
comparar: (a: ElementoOuValor<T>, b: ElementoOuValor<T>) => number;
renderizar: (item: ElementoOuValor<T>) => React.ReactNode;
}
function ListaOrdenada<T>({
items,
comparar,
renderizar
}: Comparador<T>) {
const itemsOrdenados = [...items].sort(comparar);
return (
<ul>
{itemsOrdenados.map((item, idx) => (
<li key={idx}>{renderizar(item)}</li>
))}
</ul>
);
}
// Usando com array de números
<ListaOrdenada
items={[3, 1, 4, 1, 5, 9]}
comparar={(a, b) => a - b}
renderizar={(num) => Número: ${num}}
/>
// Usando com array de usuários
interface Usuario {
nome: string;
idade: number;
}
<ListaOrdenada
items={[{ nome: "Ana", idade: 25 }, { nome: "Bruno", idade: 20 }]}
comparar={(a, b) => a.idade - b.idade}
renderizar={(usuario) => ${usuario.nome} - ${usuario.idade} anos}
/></code></pre>
<h3>Injeção de Dependências com Genéricos</h3>
<p>Para aplicações maiores, você pode criar padrões de injeção de dependência usando genéricos:</p>
<pre><code class="language-typescript">interface Servico<T> {
obter: () => Promise<T[]>;
criar: (item: T) => Promise<T>;
deletar: (id: number) => Promise<void>;
}
interface ProvedorServicoProps<T> {
servico: Servico<T>;
children: (estado: {
dados: T[];
carregando: boolean;
erro: Error | null;
criar: (item: T) => Promise<void>;
deletar: (id: number) => Promise<void>;
}) => React.ReactNode;
}
function ProvedorServico<T>({ servico, children }: ProvedorServicoProps<T>) {
const [dados, setDados] = React.useState<T[]>([]);
const [carregando, setCarregando] = React.useState(true);
const [erro, setErro] = React.useState<Error | null>(null);
React.useEffect(() => {
servico.obter()
.then(setDados)
.catch(setErro)
.finally(() => setCarregando(false));
}, [servico]);
const criar = async (item: T) => {
const novo = await servico.criar(item);
setDados(prev => [...prev, novo]);
};
const deletar = async (id: number) => {
await servico.deletar(id);
setDados(prev => prev.filter((_, i) => i !== id));
};
return <>{children({ dados, carregando, erro, criar, deletar })}</>;
}
// Uso
interface Post {
id: number;
titulo: string;
corpo: string;
}
const servicoPosts: Servico<Post> = {
obter: async () => {
const resp = await fetch('/api/posts');
return resp.json();
},
criar: async (post) => {
const resp = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(post)
});
return resp.json();
},
deletar: async (id) => {
await fetch(/api/posts/${id}, { method: 'DELETE' });
}
};
export default function App() {
return (
<ProvedorServico<Post> servico={servicoPosts}>
{({ dados, carregando, criar }) => (
<div>
{carregando ? <p>Carregando...</p> : null}
{dados.map(post => (
<article key={post.id}>
<h3>{post.titulo}</h3>
<p>{post.corpo}</p>
</article>
))}
</div>
)}
</ProvedorServico>
);
}</code></pre>
<h2>Conclusão</h2>
<p>Ao dominar componentes genéricos em React com TypeScript, você ganha três superpoderes principais. Primeiro, a <strong>reutilização genuína de código</strong>: um único componente funciona perfeitamente para dezenas de tipos diferentes sem comprometer a segurança de tipos. Segundo, a <strong>segurança em tempo de compilação</strong>: erros são capturados antes do código rodar em produção, reduzindo bugs significativamente. Terceiro, a <strong>experiência do desenvolvedor melhorada</strong>: IDEs fornecem autocompletar preciso e inferência de tipo inteligente porque o TypeScript entende exatamente qual tipo está sendo usado.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/generics.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Generics</a></li>
<li><a href="https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/basic_type_example/" target="_blank" rel="noopener noreferrer">React TypeScript Cheatsheet - Generics</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html" target="_blank" rel="noopener noreferrer">Advanced TypeScript - Conditional Types</a></li>
<li><a href="https://react.dev/reference/react/useContext" target="_blank" rel="noopener noreferrer">React Documentation - Context API</a></li>
<li><a href="https://www.leveluptutorials.com/tutorials/typescript-tutorial" target="_blank" rel="noopener noreferrer">Wes Bos - TypeScript Course</a></li>
</ul>
<p><!-- FIM --></p>