SOLID no Frontend: aplicando os princípios em componentes React
Como os 5 princípios SOLID se traduzem para o mundo dos componentes React com TypeScript, com exemplos práticos do dia a dia.
SOLID não é só para o backend
Durante minha passagem pela SenseData, apliquei intensamente os princípios SOLID para separar responsabilidades entre lógica e UI em uma aplicação SaaS densa. O resultado foi direto: menos re-renders, manutenção mais fácil e features entregues mais rápido.
Vamos ver como cada princípio se traduz para o mundo React + TypeScript.
S — Single Responsibility
Cada componente deve ter uma razão para mudar. O erro mais comum é o componente "faz-tudo":
// ❌ Violando SRP
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser).finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
return (
<div>
<img src={user.avatar} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
// ✅ Respeitando SRP
function useUser(userId: string) {
return useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) });
}
function UserCard({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId);
if (isLoading) return <Spinner />;
return <UserCardView user={user} />;
}
function UserCardView({ user }: { user: User }) {
return (
<div>
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
O — Open/Closed
Aberto para extensão, fechado para modificação. Em React, isso se traduz em composição via props:
// ✅ Componente extensível via composition
function Button({ children, icon, variant = 'primary', ...props }: ButtonProps) {
return (
<button className={variants[variant]} {...props}>
{icon && <span aria-hidden="true">{icon}</span>}
{children}
</button>
);
}
// Extensão sem modificar o Button original
<Button icon={<WhatsappLogo />} variant="success">Falar no WhatsApp</Button>
L — Liskov Substitution
Subtipos devem ser substituíveis pelos seus tipos base. No React, isso significa que componentes que recebem React.ComponentProps devem se comportar como o elemento nativo esperado:
// ✅ Input customizado que respeita LSP
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
function Input({ label, error, id, ...nativeProps }: InputProps) {
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} aria-invalid={!!error} {...nativeProps} />
{error && <span role="alert">{error}</span>}
</div>
);
}
I — Interface Segregation
Não force componentes a depender de props que não usam:
// ❌ Interface gordurosa
interface CardProps {
title: string;
description: string;
imageUrl: string;
author: string;
date: string;
onLike: () => void;
onShare: () => void;
onComment: () => void;
}
// ✅ Interfaces segregadas por responsabilidade
interface CardBaseProps { title: string; description: string; }
interface CardMediaProps { imageUrl: string; }
interface CardActionsProps { onLike?: () => void; onShare?: () => void; }
D — Dependency Inversion
Dependa de abstrações, não de implementações concretas. Em React, isso significa injetar dependências via props ou contexto:
// ✅ Componente não sabe como os dados chegam
interface UserListProps {
fetchUsers: () => Promise<User[]>;
}
function UserList({ fetchUsers }: UserListProps) {
const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
// ...
}
// Produção
<UserList fetchUsers={() => api.get('/users')} />
// Teste
<UserList fetchUsers={() => Promise.resolve(mockUsers)} />
Conclusão
SOLID no frontend não é dogma, é guia. Aplicado com bom senso, resulta em componentes menores, mais testáveis e mais fáceis de manter. O maior ganho que obtive foi na velocidade de onboarding: novos devs entendem o código muito mais rápido quando cada peça tem uma responsabilidade clara.