<h2>O Que São Hooks Customizados e Por Que Importam</h2>
<p>Hooks customizados são funções JavaScript que reutilizam lógica com estado e efeitos colaterais, encapsulando comportamentos complexos em unidades testáveis e compostas. No React, quando você percebe que está repetindo a mesma lógica de gerenciamento de estado em múltiplos componentes, é hora de extrair essa lógica para um hook customizado. A essência aqui não é apenas evitar código duplicado, mas criar abstrações que tornem seu código mais legível, testável e manutenível.</p>
<p>A diferença fundamental entre hooks customizados e componentes é que hooks retornam apenas dados e funções, enquanto componentes retornam JSX. Isso significa que você pode compor múltiplos hooks dentro de um componente ou até combinar hooks customizados dentro de outros hooks customizados. Essa composição é o cerne da arquitetura moderna com React, permitindo que você construa sistemas complexos a partir de pequenas peças bem definidas.</p>
<h2>Princípios de Composição em Hooks Customizados</h2>
<h3>Composição Horizontal vs. Vertical</h3>
<p>Composição horizontal refere-se ao uso de múltiplos hooks dentro de um único componente, cada um responsável por um aspecto diferente da lógica. Por exemplo, um hook para gerenciar autenticação, outro para buscar dados, e mais um para rastreamento de erros — todos vivendo lado a lado no mesmo componente. Composição vertical é quando você cria hooks que dependem de outros hooks, formando uma hierarquia de abstrações.</p>
<p>A chave para uma boa composição é garantir que cada hook tenha uma responsabilidade única e bem definida. Violações do Single Responsibility Principle (SRP) resultam em hooks inflexíveis que são difíceis de testar e reutilizar. Veja um exemplo prático:</p>
<pre><code class="language-javascript"></code></pre>
<h3>Invertendo o Controle com Callbacks</h3>
<p>Um padrão poderoso é permitir que quem usar seu hook customize seu comportamento através de callbacks. Isso inverte a dependência: em vez de o hook ser acoplado a casos de uso específicos, é o componente que injeta seu próprio comportamento.</p>
<pre><code class="language-javascript">// Hook genérico para qualquer tipo de requisição
function useFetch(url, onSuccess, onError) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch(url)
.then(r => r.json())
.then(result => {
setData(result);
onSuccess?.(result);
})
.catch(err => {
onError?.(err);
})
.finally(() => setLoading(false));
}, [url, onSuccess, onError]);
return { data, loading };
}
// Reutilização em diferentes contextos
function ProductList() {
const { data: products, loading } = useFetch(
'/api/products',
(data) => console.log('Produtos carregados:', data),
(err) => console.error('Erro ao carregar produtos:', err)
);
return loading ? <div>Carregando...</div> : <div>{products?.length} produtos</div>;
}
function ArticleList() {
const { data: articles, loading } = useFetch(
'/api/articles',
(data) => analytics.track('articles_loaded', { count: data.length }),
(err) => notificationService.showError('Falha ao carregar artigos')
);
return loading ? <div>Carregando...</div> : <div>{articles?.length} artigos</div>;
}</code></pre>
<h2>Separação de Concerns em Prática</h2>
<h3>Isolando Lógica de Apresentação da Lógica de Negócio</h3>
<p>Separação de concerns significa que cada entidade do seu código tem uma razão única para mudar. Hooks customizados são o mecanismo perfeito para extrair a lógica de negócio dos componentes, deixando estes últimos focados apenas em renderização e interação com o usuário.</p>
<p>Considere um formulário de autenticação. A lógica de validação, chamadas à API e gerenciamento de estado são preocupações de negócio. A renderização de inputs, botões e mensagens de erro são preocupações de apresentação. Separando-as:</p>
<pre><code class="language-javascript">// Concern 1: Lógica de autenticação (pode ser testado sem React)
function useAuthLogic(onAuthSuccess) {
const [credentials, setCredentials] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validate = useCallback((email, password) => {
const newErrors = {};
if (!email.includes('@')) newErrors.email = 'Email inválido';
if (password.length < 8) newErrors.password = 'Mínimo 8 caracteres';
return newErrors;
}, []);
const handleSubmit = useCallback(async (e) => {
e?.preventDefault();
const newErrors = validate(credentials.email, credentials.password);
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
setIsSubmitting(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
localStorage.setItem('token', data.token);
onAuthSuccess(data.user);
} catch (err) {
setErrors({ submit: err.message });
} finally {
setIsSubmitting(false);
}
}
}, [credentials, validate, onAuthSuccess]);
return {
credentials,
setCredentials,
errors,
isSubmitting,
handleSubmit
};
}
// Concern 2: Renderização e interação com usuário
function LoginForm() {
const { credentials, setCredentials, errors, isSubmitting, handleSubmit } =
useAuthLogic((user) => {
// Navegar ou atualizar contexto global
console.log('Usuário autenticado:', user);
});
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials(prev => ({
...prev,
email: e.target.value
}))}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials(prev => ({
...prev,
password: e.target.value
}))}
placeholder="Senha"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
{errors.submit && <div className="error">{errors.submit}</div>}
<button disabled={isSubmitting}>
{isSubmitting ? 'Autenticando...' : 'Entrar'}
</button>
</form>
);
}</code></pre>
<h3>Hooks para Gerenciamento de Estado Derivado</h3>
<p>Muitas vezes você precisa derivar novo estado a partir de um estado existente. Criar um hook para isso mantém essa transformação isolada e testável, evitando cálculos repetidos no componente.</p>
<pre><code class="language-javascript">// Hook que encapsula lógica de derivação de estado
function useFilteredAndSortedItems(items, filterFn, sortFn) {
const filtered = useMemo(
() => items.filter(filterFn),
[items, filterFn]
);
const sorted = useMemo(
() => [...filtered].sort(sortFn),
[filtered, sortFn]
);
const stats = useMemo(
() => ({
total: items.length,
filtered: filtered.length,
percentage: (filtered.length / items.length * 100).toFixed(1)
}),
[items.length, filtered.length]
);
return { sorted, stats };
}
// Componente focado apenas em renderização
function ProductCatalog({ products }) {
const { sorted, stats } = useFilteredAndSortedItems(
products,
(product) => product.price < 100,
(a, b) => a.name.localeCompare(b.name)
);
return (
<div>
<p>Mostrando {stats.filtered} de {stats.total} produtos ({stats.percentage}%)</p>
<ul>
{sorted.map(product => (
<li key={product.id}>{product.name} - R${product.price}</li>
))}
</ul>
</div>
);
}</code></pre>
<h2>Padrões Avançados e Melhores Práticas</h2>
<h3>Composição de Hooks com Custom Reducers</h3>
<p>Para lógica mais complexa envolvendo múltiplos estados interdependentes, combinar hooks com reducers é uma abordagem poderosa que mantém a lógica determinística e testável.</p>
<pre><code class="language-javascript">// Hook com reducer para gerenciar estado complexo
function usePaginatedData(fetchFn) {
const initialState = {
items: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
error: null
};
const reducer = (state, action) => {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return {
...state,
items: action.payload.items,
total: action.payload.total,
loading: false
};
case 'FETCH_ERROR':
return { ...state, error: action.payload, loading: false };
case 'SET_PAGE':
return { ...state, page: action.payload };
case 'SET_PAGE_SIZE':
return { ...state, pageSize: action.payload, page: 1 };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetchFn(state.page, state.pageSize)
.then((data) => {
dispatch({
type: 'FETCH_SUCCESS',
payload: data
});
})
.catch((error) => {
dispatch({
type: 'FETCH_ERROR',
payload: error.message
});
});
}, [state.page, state.pageSize, fetchFn]);
const goToPage = useCallback((page) => {
dispatch({ type: 'SET_PAGE', payload: page });
}, []);
const changePageSize = useCallback((size) => {
dispatch({ type: 'SET_PAGE_SIZE', payload: size });
}, []);
return {
...state,
goToPage,
changePageSize,
totalPages: Math.ceil(state.total / state.pageSize)
};
}
// Uso em um componente
function UserTable() {
const { items, page, pageSize, loading, error, totalPages, goToPage, changePageSize } =
usePaginatedData(async (pageNum, size) => {
const response = await fetch(/api/users?page=${pageNum}&limit=${size});
return response.json();
});
return (
<div>
{error && <div className="error">{error}</div>}
{loading && <div>Carregando...</div>}
<table>
<tbody>
{items.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
<div>
Página {page} de {totalPages}
<button onClick={() => goToPage(page - 1)} disabled={page === 1}>← Anterior</button>
<button onClick={() => goToPage(page + 1)} disabled={page === totalPages}>Próxima →</button>
</div>
<select value={pageSize} onChange={(e) => changePageSize(Number(e.target.value))}>
<option value={5}>5 por página</option>
<option value={10}>10 por página</option>
<option value={20}>20 por página</option>
</select>
</div>
);
}</code></pre>
<h3>Context + Hooks para Estado Global Sem Redux</h3>
<p>Para estado realmente global sem a overhead do Redux, Context combinado com hooks customizados é elegante e suficiente para muitos casos.</p>
<pre><code class="language-javascript">// Criar o contexto
const ThemeContext = createContext();
// Hook customizado para usar o contexto
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme deve ser usado dentro de ThemeProvider');
}
return context;
}
// Provider que encapsula a lógica de tema
function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(() => {
return localStorage.getItem('theme') === 'dark';
});
const toggleTheme = useCallback(() => {
setIsDark(prev => {
const newValue = !prev;
localStorage.setItem('theme', newValue ? 'dark' : 'light');
return newValue;
});
}, []);
const theme = useMemo(() => ({
isDark,
colors: isDark
? { bg: '#1a1a1a', text: '#ffffff' }
: { bg: '#ffffff', text: '#000000' }
}), [isDark]);
return (
<ThemeContext.Provider value={{ ...theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Uso em qualquer componente
function App() {
const { isDark, colors, toggleTheme } = useTheme();
return (
<div style={{ backgroundColor: colors.bg, color: colors.text }}>
<p>Tema escuro: {isDark ? 'Sim' : 'Não'}</p>
<button onClick={toggleTheme}>Alternar tema</button>
</div>
);
}
// No topo da árvore
function Root() {
return (
<ThemeProvider>
<App />
</ThemeProvider>
);
}</code></pre>
<h2>Testando Hooks Customizados</h2>
<p>Hooks customizados são facilmente testáveis quando bem separados. A biblioteca <code>@testing-library/react</code> fornece <code>renderHook</code> exatamente para isso.</p>
<pre><code class="language-javascript">import { renderHook, act } from '@testing-library/react';
import { useAuthLogic } from './useAuthLogic';
describe('useAuthLogic', () => {
it('valida email corretamente', () => {
const { result } = renderHook(() => useAuthLogic(() => {}));
expect(result.current.errors.email).toBeUndefined();
act(() => {
result.current.setCredentials({ email: 'invalido', password: 'senha123456' });
});
act(() => {
result.current.handleSubmit();
});
expect(result.current.errors.email).toBe('Email inválido');
});
it('envia dados corretos para a API', async () => {
const mockFetch = jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: () => Promise.resolve({ token: 'abc123', user: { id: 1 } })
});
const onSuccess = jest.fn();
const { result } = renderHook(() => useAuthLogic(onSuccess));
act(() => {
result.current.setCredentials({ email: 'user@email.com', password: 'senha123456' });
});
await act(async () => {
await result.current.handleSubmit();
});
expect(mockFetch).toHaveBeenCalledWith(
'/api/auth/login',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ email: 'user@email.com', password: 'senha123456' })
})
);
expect(onSuccess).toHaveBeenCalledWith({ id: 1 });
mockFetch.mockRestore();
});
});</code></pre>
<h2>Conclusão</h2>
<p>Durante este artigo, exploramos três pilares essenciais para dominar hooks customizados. Primeiro, compreender que <strong>composição e separação de concerns não são apenas boas práticas — são a base para código escalável e sustentável</strong>. Um hook bem construído é pequeno, testável e reutilizável porque tem uma responsabilidade única e bem definida. Segundo, <strong>invertendo o controle através de callbacks e propondo abstrações genéricas, você cria hooks que servem múltiplos casos de uso sem repetir lógica</strong>. Terceiro, os padrões avançados como reducers e Context mostram que hooks customizados vão muito além de um simples wrapper: eles são o mecanismo através do qual você constrói arquiteturas React profissionais, sem necessidade de bibliotecas externas para problemas bem delimitados.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/reference/react/hooks" target="_blank" rel="noopener noreferrer">React Hooks Documentation - Official</a></li>
<li><a href="https://usehooks.com/" target="_blank" rel="noopener noreferrer">useHooks - A Collection of React Hooks Recipes</a></li>
<li><a href="https://testing-library.com/docs/react-testing-library/api/#renderhook" target="_blank" rel="noopener noreferrer">Testing Library - renderHook</a></li>
<li><a href="https://martinfowler.com/bliki/SeparationOfConcerns.html" target="_blank" rel="noopener noreferrer">Martin Fowler - Separation of Concerns</a></li>
<li><a href="https://kentcdodds.com/blog/advanced-react-component-patterns" target="_blank" rel="noopener noreferrer">Advanced React Component Patterns - Kent C. Dodds</a></li>
</ul>
<p><!-- FIM --></p>