Aprenda a compor custom hooks no React para abstrair lógicas complexas, melhorar a reutilização de código e a manutenibilidade dos seus projetos. Inclui exemplos práticos e boas práticas.
Composição de Custom Hooks no React: Dominando a Abstração de Lógica Complexa
Os custom hooks do React são uma ferramenta poderosa para encapsular e reutilizar lógica com estado nas suas aplicações React. No entanto, à medida que suas aplicações crescem em complexidade, o mesmo acontece com a lógica dentro dos seus custom hooks. Isso pode levar a hooks monolíticos que são difíceis de entender, testar e manter. A composição de custom hooks oferece uma solução para este problema, permitindo que você divida lógicas complexas em hooks menores, mais gerenciáveis e reutilizáveis.
O que é Composição de Custom Hooks?
A composição de custom hooks é a prática de combinar vários custom hooks menores para criar funcionalidades mais complexas. Em vez de criar um único hook grande que lida com tudo, você cria vários hooks menores, cada um responsável por um aspeto específico da lógica. Esses hooks menores podem então ser compostos para alcançar a funcionalidade desejada.
Pense nisso como construir com blocos de LEGO. Cada bloco (hook pequeno) tem uma função específica, e você os combina de várias maneiras para construir estruturas complexas (funcionalidades maiores).
Benefícios da Composição de Custom Hooks
- Reutilização de Código Aprimorada: Hooks menores e mais focados são inerentemente mais reutilizáveis em diferentes componentes e até mesmo em diferentes projetos.
- Manutenibilidade Melhorada: Dividir lógicas complexas em unidades menores e autocontidas torna mais fácil entender, depurar e modificar seu código. Alterações em um hook têm menos probabilidade de afetar outras partes da sua aplicação.
- Testabilidade Aumentada: Hooks menores são mais fáceis de testar isoladamente, levando a um código mais robusto e confiável.
- Melhor Organização de Código: A composição incentiva uma base de código mais modular e organizada, facilitando a navegação e a compreensão das relações entre as diferentes partes da sua aplicação.
- Redução da Duplicação de Código: Ao extrair lógicas comuns para hooks reutilizáveis, você minimiza a duplicação de código, resultando em uma base de código mais concisa e de fácil manutenção.
Quando Usar a Composição de Custom Hooks
Você deve considerar o uso da composição de custom hooks quando:
- Um único custom hook está a tornar-se demasiado grande e complexo.
- Você se vê a duplicar lógica semelhante em vários custom hooks ou componentes.
- Você deseja melhorar a testabilidade dos seus custom hooks.
- Você quer criar uma base de código mais modular e reutilizável.
Princípios Básicos da Composição de Custom Hooks
Aqui estão alguns princípios-chave para guiar a sua abordagem à composição de custom hooks:
- Princípio da Responsabilidade Única: Cada custom hook deve ter uma única responsabilidade bem definida. Isso torna-os mais fáceis de entender, testar e reutilizar.
- Separação de Preocupações: Separe diferentes aspetos da sua lógica em diferentes hooks. Por exemplo, você pode ter um hook para buscar dados, outro para gerenciar o estado e outro para lidar com efeitos colaterais.
- Composabilidade: Projete seus hooks para que possam ser facilmente compostos com outros hooks. Isso geralmente envolve retornar dados ou funções que podem ser usados por outros hooks.
- Convenções de Nomenclatura: Use nomes claros e descritivos para seus hooks para indicar seu propósito e funcionalidade. Uma convenção comum é prefixar os nomes dos hooks com `use`.
Padrões Comuns de Composição
Vários padrões podem ser usados para compor custom hooks. Aqui estão alguns dos mais comuns:
1. Composição Simples de Hooks
Esta é a forma mais básica de composição, onde um hook simplesmente chama outro hook e usa seu valor de retorno.
Exemplo: Imagine que você tem um hook para buscar dados de um utilizador e outro para formatar datas. Você pode compor esses hooks para criar um novo hook que busca os dados do utilizador e formata a data de registo do utilizador.
import { useState, useEffect } from 'react';
function useUserData(userId) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const jsonData = await response.json();
setData(jsonData);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
fetchData();
}, [userId]);
return { data, loading, error };
}
function useFormattedDate(dateString) {
try {
const date = new Date(dateString);
const formattedDate = date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
return formattedDate;
} catch (error) {
console.error("Error formatting date:", error);
return "Invalid Date";
}
}
function useUserWithFormattedDate(userId) {
const { data, loading, error } = useUserData(userId);
const formattedRegistrationDate = data ? useFormattedDate(data.registrationDate) : null;
return { ...data, formattedRegistrationDate, loading, error };
}
export default useUserWithFormattedDate;
Explicação:
useUserDatabusca dados de utilizador a partir de uma API.useFormattedDateformata uma string de data para um formato amigável. Ele lida com possíveis erros de análise de data de forma elegante. O argumento `undefined` para `toLocaleDateString` usa a localidade do utilizador para a formatação.useUserWithFormattedDatecompõe ambos os hooks. Primeiro, ele usauseUserDatapara buscar os dados do utilizador. Depois, se os dados estiverem disponíveis, ele usauseFormattedDatepara formatar aregistrationDate. Finalmente, ele retorna os dados originais do utilizador juntamente com a data formatada, o estado de carregamento e quaisquer erros potenciais.
2. Composição de Hooks com Estado Compartilhado
Neste padrão, vários hooks compartilham e modificam o mesmo estado. Isso pode ser alcançado usando useContext ou passando o estado e as funções de atualização entre os hooks.
Exemplo: Imagine a construção de um formulário de várias etapas. Cada etapa poderia ter seu próprio hook para gerenciar os campos de entrada e a lógica de validação específicos da etapa, mas todos compartilham um estado de formulário comum gerenciado por um hook pai usando useReducer e useContext.
import React, { createContext, useContext, useReducer } from 'react';
// Define o estado inicial
const initialState = {
step: 1,
name: '',
email: '',
address: ''
};
// Define as ações
const ACTIONS = {
NEXT_STEP: 'NEXT_STEP',
PREVIOUS_STEP: 'PREVIOUS_STEP',
UPDATE_FIELD: 'UPDATE_FIELD'
};
// Cria o reducer
function formReducer(state, action) {
switch (action.type) {
case ACTIONS.NEXT_STEP:
return { ...state, step: state.step + 1 };
case ACTIONS.PREVIOUS_STEP:
return { ...state, step: state.step - 1 };
case ACTIONS.UPDATE_FIELD:
return { ...state, [action.payload.field]: action.payload.value };
default:
return state;
}
}
// Cria o contexto
const FormContext = createContext();
// Cria um componente provedor
function FormProvider({ children }) {
const [state, dispatch] = useReducer(formReducer, initialState);
const value = {
state,
dispatch,
nextStep: () => dispatch({ type: ACTIONS.NEXT_STEP }),
previousStep: () => dispatch({ type: ACTIONS.PREVIOUS_STEP }),
updateField: (field, value) => dispatch({ type: ACTIONS.UPDATE_FIELD, payload: { field, value } })
};
return (
{children}
);
}
// Hook personalizado para aceder ao contexto do formulário
function useFormContext() {
const context = useContext(FormContext);
if (!context) {
throw new Error('useFormContext must be used within a FormProvider');
}
return context;
}
// Hook personalizado para a Etapa 1
function useStep1() {
const { state, updateField } = useFormContext();
const updateName = (value) => updateField('name', value);
return {
name: state.name,
updateName
};
}
// Hook personalizado para a Etapa 2
function useStep2() {
const { state, updateField } = useFormContext();
const updateEmail = (value) => updateField('email', value);
return {
email: state.email,
updateEmail
};
}
// Hook personalizado para a Etapa 3
function useStep3() {
const { state, updateField } = useFormContext();
const updateAddress = (value) => updateField('address', value);
return {
address: state.address,
updateAddress
};
}
export { FormProvider, useFormContext, useStep1, useStep2, useStep3 };
Explicação:
- Um
FormContexté criado usandocreateContextpara conter o estado do formulário e a função de dispatch. - Um
formReducergerencia as atualizações de estado do formulário usandouseReducer. Ações comoNEXT_STEP,PREVIOUS_STEPeUPDATE_FIELDsão definidas para modificar o estado. - O componente
FormProviderfornece o contexto do formulário para seus filhos, tornando o estado e o dispatch disponíveis para todas as etapas do formulário. Ele também expõe funções auxiliares para `nextStep`, `previousStep` e `updateField` para simplificar o dispatch de ações. - O hook
useFormContextpermite que os componentes acedam aos valores do contexto do formulário. - Cada etapa (
useStep1,useStep2,useStep3) cria seu próprio hook para gerenciar a entrada relacionada à sua etapa e usauseFormContextpara obter o estado e a função de dispatch para atualizá-lo. Cada etapa expõe apenas os dados e as funções relevantes para essa etapa, aderindo ao princípio da responsabilidade única.
3. Composição de Hooks com Gerenciamento de Ciclo de Vida
Este padrão envolve hooks que gerenciam diferentes fases do ciclo de vida de um componente, como montagem, atualização e desmontagem. Isso é frequentemente alcançado usando useEffect dentro dos hooks compostos.
Exemplo: Considere um componente que precisa rastrear o status online/offline e também precisa realizar alguma limpeza quando é desmontado. Você pode criar hooks separados para cada uma dessas tarefas e depois compô-los.
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
return () => {
document.title = 'Original Title'; // Reverte para um título padrão na desmontagem
};
}, [title]);
}
function useAppLifecycle(title) {
const isOnline = useOnlineStatus();
useDocumentTitle(title);
return isOnline; // Retorna o status online
}
export { useAppLifecycle, useOnlineStatus, useDocumentTitle };
Explicação:
useOnlineStatusrastreia o status online do utilizador usando os eventosonlineeoffline. O hookuseEffectconfigura os ouvintes de eventos quando o componente é montado e os limpa quando é desmontado.useDocumentTitleatualiza o título do documento. Ele também reverte o título para um valor padrão quando o componente é desmontado, garantindo que não haja problemas de título persistentes.useAppLifecyclecompõe ambos os hooks. Ele usauseOnlineStatuspara determinar se o utilizador está online euseDocumentTitlepara definir o título do documento. O hook combinado retorna o status online.
Exemplos Práticos e Casos de Uso
1. Internacionalização (i18n)
Gerenciar traduções e troca de localidade pode se tornar complexo. Você pode usar a composição de hooks para separar as preocupações:
useLocale(): Gerencia a localidade atual.useTranslations(): Busca e fornece traduções para a localidade atual.useTranslate(key): Um hook que recebe uma chave de tradução e retorna a string traduzida, usando o hookuseTranslationspara aceder às traduções.
Isso permite que você troque facilmente de localidade e aceda a traduções em toda a sua aplicação. Considere usar bibliotecas como i18next juntamente com hooks personalizados para gerenciar a lógica de tradução. Por exemplo, useTranslations poderia carregar traduções com base na localidade selecionada de arquivos JSON em diferentes idiomas.
2. Validação de Formulários
Formulários complexos geralmente exigem validação extensiva. Você pode usar a composição de hooks para criar lógicas de validação reutilizáveis:
useInput(initialValue): Gerencia o estado de um único campo de entrada.useValidator(value, rules): Valida um único campo de entrada com base em um conjunto de regras (ex: obrigatório, e-mail, comprimento mínimo).useForm(fields): Gerencia o estado e a validação de todo o formulário, compondouseInputeuseValidatorpara cada campo.
Essa abordagem promove a reutilização de código e facilita a adição ou modificação de regras de validação. Bibliotecas como Formik ou React Hook Form fornecem soluções pré-construídas, mas podem ser aumentadas com hooks personalizados para necessidades de validação específicas.
3. Busca e Cache de Dados
Gerenciar a busca de dados, cache e tratamento de erros pode ser simplificado com a composição de hooks:
useFetch(url): Busca dados de uma URL fornecida.useCache(key, fetchFunction): Armazena em cache o resultado de uma função de busca usando uma chave.useData(url, options): CombinauseFetcheuseCachepara buscar dados e armazenar os resultados em cache.
Isso permite que você armazene facilmente em cache dados acedidos com frequência e melhore o desempenho. Bibliotecas como SWR (Stale-While-Revalidate) e React Query fornecem soluções poderosas de busca e cache de dados que podem ser estendidas com hooks personalizados.
4. Autenticação
Lidar com a lógica de autenticação pode ser complexo, especialmente ao lidar com diferentes métodos de autenticação (ex: JWT, OAuth). A composição de hooks pode ajudar a separar diferentes aspetos do processo de autenticação:
useAuthToken(): Gerencia o token de autenticação (ex: armazenando e recuperando-o do armazenamento local).useUser(): Busca e fornece as informações do utilizador atual com base no token de autenticação.useAuth(): Fornece funções relacionadas à autenticação como login, logout e signup, compondo os outros hooks.
Essa abordagem permite que você troque facilmente entre diferentes métodos de autenticação ou adicione novas funcionalidades ao processo de autenticação. Bibliotecas como Auth0 e Firebase Authentication podem ser usadas como backend para gerenciar contas de utilizador e autenticação, e hooks personalizados podem ser criados para interagir com esses serviços.
Boas Práticas para a Composição de Custom Hooks
- Mantenha os Hooks Focados: Cada hook deve ter um propósito claro e específico.
- Evite Aninhamento Profundo: Limite o número de níveis de composição para evitar tornar seu código difícil de entender. Se um hook se tornar muito complexo, considere dividi-lo ainda mais.
- Documente Seus Hooks: Forneça documentação clara e concisa para cada hook, explicando seu propósito, entradas e saídas. Isso é especialmente importante para hooks que são usados por outros desenvolvedores.
- Teste Seus Hooks: Escreva testes unitários para cada hook para garantir que ele esteja a funcionar corretamente. Isso é especialmente importante para hooks que gerenciam estado ou realizam efeitos colaterais.
- Considere Usar uma Biblioteca de Gerenciamento de Estado: Para cenários complexos de gerenciamento de estado, considere usar uma biblioteca como Redux, Zustand ou Jotai. Essas bibliotecas fornecem recursos mais avançados para gerenciar estado e podem simplificar a composição de hooks.
- Pense no Tratamento de Erros: Implemente um tratamento de erros robusto em seus hooks para prevenir comportamentos inesperados. Considere usar blocos try-catch para capturar erros e fornecer mensagens de erro informativas.
- Considere o Desempenho: Esteja ciente das implicações de desempenho de seus hooks. Evite re-renderizações desnecessárias e otimize seu código para o desempenho. Use React.memo, useMemo e useCallback para otimizar o desempenho quando apropriado.
Conclusão
A composição de custom hooks no React é uma técnica poderosa para abstrair lógicas complexas e melhorar a reutilização de código, a manutenibilidade e a testabilidade. Ao dividir tarefas complexas em hooks menores e mais gerenciáveis, você pode criar uma base de código mais modular e organizada. Seguindo as boas práticas descritas neste artigo, você pode aproveitar efetivamente a composição de custom hooks para construir aplicações React robustas e escaláveis. Lembre-se de sempre priorizar a clareza e a simplicidade em seu código e não tenha medo de experimentar diferentes padrões de composição para encontrar o que funciona melhor para suas necessidades específicas.