React & Frontend

Dominando Anatomia de Hooks Customizados: Composição e Separação de Concerns em Projetos Reais

14 min de leitura

Dominando Anatomia de Hooks Customizados: Composição e Separação de Concerns em Projetos Reais

O Que São Hooks Customizados e Por Que Importam 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. 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. Princípios de Composição em Hooks Customizados Composição Horizontal vs. Vertical Composição horizontal refere-se ao uso de

<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(() =&gt; {

setLoading(true);

fetch(url)

.then(r =&gt; r.json())

.then(result =&gt; {

setData(result);

onSuccess?.(result);

})

.catch(err =&gt; {

onError?.(err);

})

.finally(() =&gt; setLoading(false));

}, [url, onSuccess, onError]);

return { data, loading };

}

// Reutilização em diferentes contextos

function ProductList() {

const { data: products, loading } = useFetch(

&#039;/api/products&#039;,

(data) =&gt; console.log(&#039;Produtos carregados:&#039;, data),

(err) =&gt; console.error(&#039;Erro ao carregar produtos:&#039;, err)

);

return loading ? &lt;div&gt;Carregando...&lt;/div&gt; : &lt;div&gt;{products?.length} produtos&lt;/div&gt;;

}

function ArticleList() {

const { data: articles, loading } = useFetch(

&#039;/api/articles&#039;,

(data) =&gt; analytics.track(&#039;articles_loaded&#039;, { count: data.length }),

(err) =&gt; notificationService.showError(&#039;Falha ao carregar artigos&#039;)

);

return loading ? &lt;div&gt;Carregando...&lt;/div&gt; : &lt;div&gt;{articles?.length} artigos&lt;/div&gt;;

}</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: &#039;&#039;, password: &#039;&#039; });

const [errors, setErrors] = useState({});

const [isSubmitting, setIsSubmitting] = useState(false);

const validate = useCallback((email, password) =&gt; {

const newErrors = {};

if (!email.includes(&#039;@&#039;)) newErrors.email = &#039;Email inválido&#039;;

if (password.length &lt; 8) newErrors.password = &#039;Mínimo 8 caracteres&#039;;

return newErrors;

}, []);

const handleSubmit = useCallback(async (e) =&gt; {

e?.preventDefault();

const newErrors = validate(credentials.email, credentials.password);

setErrors(newErrors);

if (Object.keys(newErrors).length === 0) {

setIsSubmitting(true);

try {

const response = await fetch(&#039;/api/auth/login&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(credentials)

});

const data = await response.json();

if (!response.ok) throw new Error(data.message);

localStorage.setItem(&#039;token&#039;, 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) =&gt; {

// Navegar ou atualizar contexto global

console.log(&#039;Usuário autenticado:&#039;, user);

});

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;div&gt;

&lt;input

type=&quot;email&quot;

value={credentials.email}

onChange={(e) =&gt; setCredentials(prev =&gt; ({

...prev,

email: e.target.value

}))}

placeholder=&quot;Email&quot;

/&gt;

{errors.email &amp;&amp; &lt;span className=&quot;error&quot;&gt;{errors.email}&lt;/span&gt;}

&lt;/div&gt;

&lt;div&gt;

&lt;input

type=&quot;password&quot;

value={credentials.password}

onChange={(e) =&gt; setCredentials(prev =&gt; ({

...prev,

password: e.target.value

}))}

placeholder=&quot;Senha&quot;

/&gt;

{errors.password &amp;&amp; &lt;span className=&quot;error&quot;&gt;{errors.password}&lt;/span&gt;}

&lt;/div&gt;

{errors.submit &amp;&amp; &lt;div className=&quot;error&quot;&gt;{errors.submit}&lt;/div&gt;}

&lt;button disabled={isSubmitting}&gt;

{isSubmitting ? &#039;Autenticando...&#039; : &#039;Entrar&#039;}

&lt;/button&gt;

&lt;/form&gt;

);

}</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(

() =&gt; items.filter(filterFn),

[items, filterFn]

);

const sorted = useMemo(

() =&gt; [...filtered].sort(sortFn),

[filtered, sortFn]

);

const stats = useMemo(

() =&gt; ({

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) =&gt; product.price &lt; 100,

(a, b) =&gt; a.name.localeCompare(b.name)

);

return (

&lt;div&gt;

&lt;p&gt;Mostrando {stats.filtered} de {stats.total} produtos ({stats.percentage}%)&lt;/p&gt;

&lt;ul&gt;

{sorted.map(product =&gt; (

&lt;li key={product.id}&gt;{product.name} - R${product.price}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}</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) =&gt; {

switch (action.type) {

case &#039;FETCH_START&#039;:

return { ...state, loading: true, error: null };

case &#039;FETCH_SUCCESS&#039;:

return {

...state,

items: action.payload.items,

total: action.payload.total,

loading: false

};

case &#039;FETCH_ERROR&#039;:

return { ...state, error: action.payload, loading: false };

case &#039;SET_PAGE&#039;:

return { ...state, page: action.payload };

case &#039;SET_PAGE_SIZE&#039;:

return { ...state, pageSize: action.payload, page: 1 };

default:

return state;

}

};

const [state, dispatch] = useReducer(reducer, initialState);

useEffect(() =&gt; {

dispatch({ type: &#039;FETCH_START&#039; });

fetchFn(state.page, state.pageSize)

.then((data) =&gt; {

dispatch({

type: &#039;FETCH_SUCCESS&#039;,

payload: data

});

})

.catch((error) =&gt; {

dispatch({

type: &#039;FETCH_ERROR&#039;,

payload: error.message

});

});

}, [state.page, state.pageSize, fetchFn]);

const goToPage = useCallback((page) =&gt; {

dispatch({ type: &#039;SET_PAGE&#039;, payload: page });

}, []);

const changePageSize = useCallback((size) =&gt; {

dispatch({ type: &#039;SET_PAGE_SIZE&#039;, 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) =&gt; {

const response = await fetch(/api/users?page=${pageNum}&amp;limit=${size});

return response.json();

});

return (

&lt;div&gt;

{error &amp;&amp; &lt;div className=&quot;error&quot;&gt;{error}&lt;/div&gt;}

{loading &amp;&amp; &lt;div&gt;Carregando...&lt;/div&gt;}

&lt;table&gt;

&lt;tbody&gt;

{items.map(user =&gt; (

&lt;tr key={user.id}&gt;

&lt;td&gt;{user.name}&lt;/td&gt;

&lt;td&gt;{user.email}&lt;/td&gt;

&lt;/tr&gt;

))}

&lt;/tbody&gt;

&lt;/table&gt;

&lt;div&gt;

Página {page} de {totalPages}

&lt;button onClick={() =&gt; goToPage(page - 1)} disabled={page === 1}&gt;← Anterior&lt;/button&gt;

&lt;button onClick={() =&gt; goToPage(page + 1)} disabled={page === totalPages}&gt;Próxima →&lt;/button&gt;

&lt;/div&gt;

&lt;select value={pageSize} onChange={(e) =&gt; changePageSize(Number(e.target.value))}&gt;

&lt;option value={5}&gt;5 por página&lt;/option&gt;

&lt;option value={10}&gt;10 por página&lt;/option&gt;

&lt;option value={20}&gt;20 por página&lt;/option&gt;

&lt;/select&gt;

&lt;/div&gt;

);

}</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(&#039;useTheme deve ser usado dentro de ThemeProvider&#039;);

}

return context;

}

// Provider que encapsula a lógica de tema

function ThemeProvider({ children }) {

const [isDark, setIsDark] = useState(() =&gt; {

return localStorage.getItem(&#039;theme&#039;) === &#039;dark&#039;;

});

const toggleTheme = useCallback(() =&gt; {

setIsDark(prev =&gt; {

const newValue = !prev;

localStorage.setItem(&#039;theme&#039;, newValue ? &#039;dark&#039; : &#039;light&#039;);

return newValue;

});

}, []);

const theme = useMemo(() =&gt; ({

isDark,

colors: isDark

? { bg: &#039;#1a1a1a&#039;, text: &#039;#ffffff&#039; }

: { bg: &#039;#ffffff&#039;, text: &#039;#000000&#039; }

}), [isDark]);

return (

&lt;ThemeContext.Provider value={{ ...theme, toggleTheme }}&gt;

{children}

&lt;/ThemeContext.Provider&gt;

);

}

// Uso em qualquer componente

function App() {

const { isDark, colors, toggleTheme } = useTheme();

return (

&lt;div style={{ backgroundColor: colors.bg, color: colors.text }}&gt;

&lt;p&gt;Tema escuro: {isDark ? &#039;Sim&#039; : &#039;Não&#039;}&lt;/p&gt;

&lt;button onClick={toggleTheme}&gt;Alternar tema&lt;/button&gt;

&lt;/div&gt;

);

}

// No topo da árvore

function Root() {

return (

&lt;ThemeProvider&gt;

&lt;App /&gt;

&lt;/ThemeProvider&gt;

);

}</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 &#039;@testing-library/react&#039;;

import { useAuthLogic } from &#039;./useAuthLogic&#039;;

describe(&#039;useAuthLogic&#039;, () =&gt; {

it(&#039;valida email corretamente&#039;, () =&gt; {

const { result } = renderHook(() =&gt; useAuthLogic(() =&gt; {}));

expect(result.current.errors.email).toBeUndefined();

act(() =&gt; {

result.current.setCredentials({ email: &#039;invalido&#039;, password: &#039;senha123456&#039; });

});

act(() =&gt; {

result.current.handleSubmit();

});

expect(result.current.errors.email).toBe(&#039;Email inválido&#039;);

});

it(&#039;envia dados corretos para a API&#039;, async () =&gt; {

const mockFetch = jest.spyOn(global, &#039;fetch&#039;).mockResolvedValue({

ok: true,

json: () =&gt; Promise.resolve({ token: &#039;abc123&#039;, user: { id: 1 } })

});

const onSuccess = jest.fn();

const { result } = renderHook(() =&gt; useAuthLogic(onSuccess));

act(() =&gt; {

result.current.setCredentials({ email: &#039;user@email.com&#039;, password: &#039;senha123456&#039; });

});

await act(async () =&gt; {

await result.current.handleSubmit();

});

expect(mockFetch).toHaveBeenCalledWith(

&#039;/api/auth/login&#039;,

expect.objectContaining({

method: &#039;POST&#039;,

body: JSON.stringify({ email: &#039;user@email.com&#039;, password: &#039;senha123456&#039; })

})

);

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Como Usar Virtualização de Listas em React: react-window e react-virtual em Produção
Como Usar Virtualização de Listas em React: react-window e react-virtual em Produção

O Problema da Renderização em Listas Grandes Quando trabalha com listas conte...

O que Todo Dev Deve Saber sobre Hooks para WebSockets: Conexão Reativa e Reconexão Automática
O que Todo Dev Deve Saber sobre Hooks para WebSockets: Conexão Reativa e Reconexão Automática

Entendendo WebSockets e a Necessidade de Hooks Reativos WebSockets estabelece...

O que Todo Dev Deve Saber sobre useTransition e useOptimistic: UX de Alta Performance em React 18
O que Todo Dev Deve Saber sobre useTransition e useOptimistic: UX de Alta Performance em React 18

Introdução ao Problema de Performance em React Quando construímos aplicações...