Reutilizando lógica com Hooks personalizados

O React vem com vários Hooks embutidos como useState, useContext, e useEffect. Às vezes, você desejará que houvesse um Hook para algum propósito mais específico: Por exemplo, para buscar dados, para acompanhar se o usuário está online, ou para se conectar a uma sala de bate-papo. Você pode não encontrar esses Hooks no React, mas pode criar seus próprios Hooks para as necessidades do seu aplicativo

Você aprenderá

  • O que são Hooks personalizados e como escrever os seus próprios
  • Como reutilizar lógica entre componentes
  • Como nomear e estruturar seus Hooks personalizados
  • Quando e por que extrair Hooks personalizados

Hooks Personalizados: Compartilhando lógica entre componentes

Imagine que você está desenvolvendo um aplicativo que depende fortemente da rede (como a maioria dos aplicativos). Você deseja alertar o usuário caso a conexão de rede seja perdida acidentalmente enquanto eles estiverem usando o seu aplicativo. Como você procederia? Parece que você precisará de duas coisas no seu componente:

  1. Um estado que acompanha se a rede está online ou não.
  2. Um efeito que se inscreve nos eventos globais online e offline e atualiza o estado correspondente.

Isso manterá seu componente sincronizado com o status da rede. Você pode começar com algo assim:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  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 <h1>{isOnline ? '✅ Conectado' : '❌ Desconectado'}</h1>;
}

Tente ligar e desligar sua conexão de rede e observe como esta StatusBar é atualizada em resposta às suas ações.

Agora, imagine que você também deseja usar a mesma lógica em um componente diferente. Você deseja implementar um botão “Salvar” que ficará desativado e exibirá “Reconectando…” em vez de “Salvar” enquanto a rede estiver desligada.

Para começar, você pode copiar e colar o estado isOnline e o efeito em SaveButton:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  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);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progresso salvo');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Salvar progresso' : 'Reconectando...'}
    </button>
  );
}

Verifique que, ao desligar a rede, o botão alterará sua aparência.

Esses dois componentes funcionam bem, mas a duplicação da lógica entre eles é infeliz. Parece que, mesmo que eles tenham uma aparência visual diferente, você deseja reutilizar a lógica entre eles.

Extraindo seu próprio Hook personalizado de um componente

Imagine, por um momento, que, assim como useState e useEffect, houvesse um Hook embutido chamado useOnlineStatus. Em seguida, ambos esses componentes poderiam ser simplificados e seria possível remover a duplicação entre eles:

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Conectado' : '❌ Desconectado'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progresso salvo');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Salvar progresso' : 'Reconectando...'}
</button>
);
}

Embora não exista um Hook embutido assim, você pode escrevê-lo por conta própria. Declare uma função chamada useOnlineStatus e mova todo o código duplicado para ela, a partir dos componentes que você escreveu anteriormente:

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
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;
}

No final da função, retorne isOnline. Isso permite que seus componentes leiam esse valor:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Conectado' : '❌ Desconectado'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progresso salvo');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Salvar progresso' : 'Reconectando...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

Verifique se alternar a rede ligada e desligada atualiza ambos os componentes.

Agora, seus componentes não possuem tanta lógica repetitiva. Mais importante ainda, o código dentro deles descreve o que deles desejam fazer (usar o status online!) em vez de como fazer isso (se inscrevendo nos eventos do navegador).

Quando você extrai a lógica em Hooks personalizados, é possível ocultar os detalhes complicados de como lidar com algum sistema externo ou uma API do navegador. O código dos seus componentes expressa sua intenção, não a implementação.

Nome dos hooks sempre começam com use

Aplicações React são construídas a partir de componentes. Os componentes são construídos a partir de Hooks, sejam eles embutidos ou personalizados. Provavelmente, você frequentemente usará Hooks personalizados criados por outras pessoas, mas ocasionalmente poderá escrever um você mesmo!

Você deve seguir estas convenções de nomenclatura:

  1. Os nomes dos componentes do React devem começar com uma letra maiúscula, como StatusBar e SaveButton. Os componentes do React também precisam retornar algo que o React saiba como exibir, como um trecho de JSX.
  2. Os nomes do hooks devem começar com use seguido por uma letra maiúscula, como useState (built-in) ou useOnlineStatus (personalizado, como mencionado anteriormente na página). Hooks podem retornar valores arbitrários.

Essa convenção garante que você sempre possa olhar para um componente e saber onde seu estado, efeitos e outras funcionalidades do React podem estar “escondidos”. Por exemplo, se você vir uma chamada de função getColor() dentro do seu componente, pode ter certeza de que ela não pode conter estado do React, pois seu nome não começa com use. No entanto, uma chamada de função como useOnlineStatus() provavelmente conterá chamadas a outros Hooks internamente!

Note

Se o seu linter estiver configurado para o React, ele irá impor essa convenção de nomenclatura. Role para cima até o sandbox e renomeie useOnlineStatus para getOnlineStatus. Observe que o linter não permitirá mais que você chame useState ou useEffect dentro dele. Apenas Hooks e componentes podem chamar outros Hooks!

Deep Dive

Todos os nomes de funções chamadas durante a renderização devem começar com o prefixo use?

Não. Funções que não chamam Hooks não precisam ser Hooks.

Se sua função não chama nenhum Hook, evite o prefixo use. Em vez disso, escreva-a como uma função regular sem o prefixo use. Por exemplo, se a função useSorted abaixo não chama Hooks, você pode chamá-la de getSorted:

// 🔴 Evite: um Hook que não utiliza Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Bom: uma função regular que não utiliza Hooks
function getSorted(items) {
return items.slice().sort();
}

Isso garante que seu código possa chamar essa função regular em qualquer lugar, incluindo condições:

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ É possível chamar getSorted() condicionalmente porque não é um Hook.
displayedItems = getSorted(items);
}
// ...
}

Você deve adicionar o prefixo use a uma função (e, portanto, transformá-la em um Hook) se ela usar pelo menos um Hook em seu interior.

// ✅ Bom: um Hook que usa outros Hooks
function useAuth() {
return useContext(Auth);
}

Tecnicamente, isso não é exigido pelo React. Em princípio, é possível criar um Hook que não chama outros Hooks. Isso geralmente é confuso e limitante, então é melhor evitar esse padrão. No entanto, pode haver casos raros em que isso é útil. Por exemplo, talvez sua função não use nenhum Hook no momento, mas você planeja adicionar chamadas de Hook a ela no futuro. Nesse caso, faz sentido nomeá-la com o prefixo use.

// ✅ Bom: um Hook que provavelmente usará outros Hooks posteriormente
function useAuth() {
// TODO: Substitua por esta linha quando a autenticação for implementada:
// return useContext(Auth);
return TEST_USER;
}

Então, os componentes não poderão chamá-lo condicionalmente. Isso se tornará importante quando você realmente adicionar chamadas de Hook no interior. Se você não planeja usar Hooks dentro dele (agora ou posteriormente), não o transforme em um Hook.

Hooks personalizados permitem compartilhar lógica com estado, não o próprio estado

No exemplo anterior, quando você ligou e desligou a rede, ambos os componentes foram atualizados juntos. No entanto, é incorreto pensar que uma única variável de estado isOnline é compartilhada entre eles. Observe este código:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Ele funciona da mesma forma que antes de extrair a duplicação:

function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

Essas são duas variáveis de estado e efeitos completamente independentes! Elas acabaram tendo o mesmo valor ao mesmo tempo porque foram sincronizadas com o mesmo valor externo (se a rede está ligada ou desligada).

Para ilustrar melhor isso, precisaremos de um exemplo diferente. Considere este componente Form:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        Primeiro nome:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Último nome:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Bom dia, {firstName} {lastName}.</b></p>
    </>
  );
}

Há alguma lógica repetitiva para cada campo do formulário:

  1. Há uma variável de estado (firstName e lastName).
  2. Há um manipulador de alteração (handleFirstNameChange e handleLastNameChange).
  3. Há uma parte de JSX que especifica os atributos value e onChange para a entrada.

Você pode extrair a lógica repetitiva para este Hook personalizado useFormInput:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

Observe que ele declara apenas uma variável de estado chamada value.

No entanto, o componente Form chama useFormInput duas vezes:

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

É por isso que funciona como se estivéssemos declarando duas variáveis de estado separadas!

Os Hooks personalizados permitem compartilhar lógica com estado e não o próprio estado. Cada chamada a um Hook é completamente independente de qualquer outra chamada ao mesmo Hook. É por isso que as duas sandboxes acima são completamente equivalentes. Se desejar, role para cima e compare-as. O comportamento antes e depois de extrair um Hook personalizado é idêntico.

Quando você precisa compartilhar o próprio estado entre vários componentes, eleve-o e passe-o como propriedade em vez disso.

Passando valores reativos entre Hooks

O código dentro dos seus Hooks personalizados será executado novamente durante cada nova renderização do seu componente. É por isso que, assim como os componentes, os Hooks personalizados precisam ser puros. Pense no código dos Hooks personalizados como parte do corpo do seu componente!

Como os Hooks personalizados são renderizados juntamente com o seu componente, eles sempre recebem as props e o estado mais recentes. Para entender o que isso significa, considere este exemplo de sala de bate-papo. Altere a URL do servidor ou a sala de bate-papo:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('Nova mensagem: ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        URL do servidor:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Bem vindo(a) à sala {roomId}</h1>
    </>
  );
}

Quando você altera serverUrl ou roomId, o efeito “reage” às suas mudanças e ressincroniza. Você pode observar pelas mensagens no console que o chat se reconecta toda vez que você altera as dependências do seu efeito.

Agora mova o código do efeito para um Hook personalizado:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('Nova mensagem: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Isso permite que seu componente ChatRoom chame o seu Hook personalizado sem se preocupar com o funcionamento interno:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});

return (
<>
<label>
URL do servidor:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Bem vindo(a) à sala {roomId}</h1>
</>
);
}

Isso parece muito mais simples! (Mas faz a mesma coisa.)

Observe que a lógica ainda responde às mudanças nas props e no estado. Experimente editar a URL do servidor ou a sala selecionada:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        URL do servidor:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Bem vindo(a) à sala {roomId}</h1>
    </>
  );
}

Observe como você está recebendo o valor de retorno de um Hook:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

e passando como entrada para outro Hook:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

Sempre que o componente ChatRoom é renderizado novamente, ele passa as últimas roomId e serverUrl para o seu Hook. É por isso que o seu efeito se reconecta ao chat sempre que os valores forem diferentes após uma nova renderização. (Se você já trabalhou com software de processamento de áudio ou vídeo, encadear Hooks dessa forma pode lembrar o encadeamento de efeitos visuais ou de áudio. É como se a saída do useState “alimentasse” a entrada do useChatRoom.)

Passando manipuladores de eventos para Hooks personalizados

Under Construction

Esta seção descreve uma API experimental que ainda não foi lançada em uma versão estável do React.

Conforme você começa a usar o useChatRoom em mais componentes, pode ser desejável permitir que os componentes personalizem seu comportamento. Por exemplo, atualmente, a lógica do que fazer quando uma mensagem chega está codificada diretamente no Hook:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('Nova mensagem: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Digamos que você queira mover essa lógica de volta para o seu componente:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('Nova mensagem: ' + msg);
}
});
// ...

Para fazer isso funcionar, altere o seu Hook personalizado para receber onReceiveMessage como uma das opções nomeadas:

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ Todas as dependências declaradas
}

Isso funcionará, mas há mais uma melhoria que você pode fazer quando seu Hook personalizado aceita manipuladores de eventos.

Adicionar uma dependência em onReceiveMessage não é ideal, pois fará com que o chat se reconecte sempre que o componente for renderizado novamente. Encapsule esse manipulador de eventos em um Event Effect para removê-lo das dependências:

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Todas as dependências declaradas
}

Agora, o chat não será reconectado toda vez que o componente ChatRoom for renderizado novamente. Aqui está um exemplo completo de como passar um manipulador de eventos para um Hook personalizado com o qual você pode brincar:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('Nova mensagem: ' + msg);
    }
  });

  return (
    <>
      <label>
        URL do servidor:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Bem vindo(a) à sala {roomId}</h1>
    </>
  );
}

Observe como agora você não precisa mais saber como useChatRoom funciona para poder usá-lo. Você poderia adicioná-lo a qualquer outro componente, passar outras opções e ele funcionaria da mesma maneira. Esse é o poder dos Hooks personalizados.

Quando usar Hooks personalizados

Você não precisa extrair um Hook personalizado para cada pequeno trecho de código duplicado. Alguma duplicação é aceitável. Por exemplo, extrair um Hook useFormInput para envolver uma única chamada useState como feito anteriormente provavelmente é desnecessário.

No entanto, sempre que você escrever um Efeito, considere se seria mais claro encapsulá-lo também em um Hook personalizado. Você não deve precisar de efeitos com muita frequência, então, se você estiver escrevendo um, significa que precisa “sair do mundo React” para sincronizar com algum sistema externo ou fazer algo para o qual o React não tenha uma API embutida. encapsular o Efeito em um Hook personalizado permite que você comunique claramente sua intenção e como os dados fluem por ele.

Por exemplo, considere um componente ShippingForm que exibe dois dropdowns: um mostra a lista de cidades e outro mostra a lista de áreas na cidade selecionada. Você pode começar com um código que se parece com isso:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// Este efeito busca cidades para um país
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// Esse efeito busca as áreas para a cidade selecionada.
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);

// ...

Embora este código seja bastante repetitivo, é correto manter esses efeitos separados um do outro. Eles sincronizam duas coisas diferentes, portanto, não deve-se mesclá-los em um único efeito. Em vez disso, você pode simplificar o componente ShippingForm acima extraindo a lógica comum entre eles para o seu próprio Hook useData:

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}

Agora você pode substituir os dois efeitos nos componentes ShippingForm por chamadas ao useData:

function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...

Extrair um Hook personalizado torna o fluxo de dados explícito. Você fornece a url como entrada e obtém a data como saída. Ao “esconder” seu efeito dentro do useData, você também impede que alguém que trabalhe no componente ShippingForm adicione dependências desnecessárias a ele. Com o tempo, a maioria dos efeitos do seu aplicativo estará nos Hooks personalizados.

Deep Dive

Mantenha seus Hooks personalizados focados em casos de uso concretos de alto nível

Comece escolhendo o nome do seu Hook personalizado. Se você tiver dificuldade em escolher um nome claro, isso pode significar que seu Efeito está muito acoplado à lógica do restante do seu componente e ainda não está pronto para ser extraído.

Idealmente, o nome do seu Hook personalizado deve ser claro o suficiente para que até mesmo uma pessoa que não escreve código com frequência possa ter uma boa ideia do que seu Hook personalizado faz, o que ele recebe e o que retorna:

  • useData(url)
  • useImpressionLog(eventName, extraData)
  • useChatRoom(options)

Quando você se sincroniza com um sistema externo, o nome do seu Hook personalizado pode ser mais técnico e usar jargões específicos desse sistema. Isso é bom, desde que seja claro para uma pessoa familiarizada com esse sistema:

  • useMediaQuery(query)
  • useSocket(url)
  • useIntersectionObserver(ref, options)

Mantenha os Hooks personalizados focados em casos de uso concretos de alto nível. Evite criar e usar Hooks personalizados de “ciclo de vida” que atuem como alternativas e encapsuladores de conveniência para a própria API useEffect:

  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

Por exemplo, este Hook useMount tenta garantir que determinado código seja executado apenas “no momento da montagem”:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// 🔴 Evite: usar Hooks personalizados de "ciclo de vida"
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔴 Evite: criar Hooks personalizados de "ciclo de vida"
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook `useEffect` está com uma dependência faltando: 'fn'
}

Hooks personalizados de “ciclo de vida”, como useMount, não se encaixam bem no paradigma do React. Por exemplo, este exemplo de código contém um erro (ele não “reage” às alterações em roomId ou serverUrl), mas o linter não irá alertá-lo sobre isso porque o linter verifica apenas chamadas diretas de useEffect. Ele não saberá sobre o seu Hook.

Se você está escrevendo um efeito, comece usando a API do React diretamente:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Bom: dois efeitos separados por finalidade

useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

Em seguida, você pode (mas não é obrigatório) extrair Hooks personalizados para diferentes casos de uso de alto nível:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Ótimo: Hooks personalizados nomeados de acordo com sua finalidade
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

Um bom Hook personalizado torna o código de chamada mais declarativo, restringindo o que ele faz. Por exemplo, useChatRoom(options) pode apenas se conectar à sala de bate-papo, enquanto useImpressionLog(eventName, extraData) pode apenas enviar um registro de impressão para a análise. Se a API do seu Hook personalizado não restringir os casos de uso e for muito abstrata, a longo prazo é provável que introduza mais problemas do que resolve.

Hooks personalizados ajudam na migração para padrões melhores

Os efeitos são uma “porta de escape”: você os utiliza quando precisa “sair do React” e não há uma solução interna melhor para o seu caso de uso. Com o tempo, o objetivo da equipe do React é reduzir ao mínimo o número de efeitos em seu aplicativo, fornecendo soluções mais específicas para problemas mais específicos. Encapsular seus efeitos em Hooks personalizados facilita a atualização do seu código quando essas soluções estiverem disponíveis.

Vamos voltar a este exemplo:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  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;
}

No exemplo acima, useOnlineStatus é implementado com um par de useState e useEffect. No entanto, essa não é a melhor solução possível. Existem alguns casos específicos que não são considerados. Por exemplo, assume-se que quando o componente é montado, isOnline já é true, mas isso pode estar errado se a rede já estiver offline. Você pode usar a API do navegador navigator.onLine para verificar isso, mas usá-la diretamente não funcionaria no servidor para gerar o HTML inicial. Em resumo, este código pode ser aprimorado.

Felizmente, o React 18 inclui uma API dedicada chamada useSyncExternalStore que cuida de todos esses problemas para você. Aqui está como o seu Hook useOnlineStatus pode ser reescrito para aproveitar essa nova API:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // Como obter o valor no cliente
    () => true // Como obter o valor no servidor
  );
}

Observe como você não precisou alterar nenhum dos componentes para fazer essa migração:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Este é outro motivo pelo qual envolver efeitos em Hooks personalizados frequentemente é benéfico:

  1. Você torna o fluxo de dados de ida e volta dos seus efeitos muito explícito.
  2. Você permite que seus componentes se concentrem na intenção em vez de na implementação exata dos seus efeitos.
  3. Quando o React adiciona novos recursos, você pode remover esses efeitos sem precisar alterar nenhum dos seus componentes.

Similar a um sistema de design, você pode achar útil começar a extrair idiomatismos comuns dos componentes do seu aplicativo em Hooks personalizados. Isso manterá o código dos seus componentes focado na intenção e permitirá evitar escrever efeitos brutos com frequência. Muitos Hooks personalizados excelentes são mantidos pela comunidade do React.

Deep Dive

O React fornecerá alguma solução interna para busca de dados?

Ainda estamos trabalhando nos detalhes, mas esperamos que no futuro você escreva a busca de dados da seguinte forma:

import { use } from 'react'; // Não disponível ainda!

function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...

Se você usar Hooks personalizados como useData mencionado acima em seu aplicativo, será necessário fazer menos alterações para migrar para a abordagem eventualmente recomendada do que se você escrever efeitos brutos em cada componente manualmente. No entanto, a abordagem antiga ainda funcionará bem, então, se você se sentir confortável escrevendo efeitos brutos, pode continuar fazendo isso.

Há mais de uma maneira de fazer isso

Vamos supor que você queira implementar uma animação de fade-in do zero usando a API do navegador requestAnimationFrame. Você pode começar com um efeito que configura um loop de animação. Durante cada quadro da animação, você poderia alterar a opacidade do nó do DOM que você mantém em uma referência (ref) até que ela atinja o valor 1. Seu código pode começar assim:

import { useState, useEffect, useRef } from 'react';

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // Ainda temos mais quadros para renderizar
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Bem vindo(a)
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remover' : 'Mostrar'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Para tornar o componente mais legível, você pode extrair a lógica para um Hook personalizado useFadeIn:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Bem vindo(a)
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remover' : 'Mostrar'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Você pode manter o código do useFadeIn como está, mas também pode refatorá-lo ainda mais. Por exemplo, você pode extrair a lógica para configurar o loop de animação do useFadeIn para um Hook personalizado useAnimationLoop:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

No entanto, você não precisou fazer isso. Assim como com funções regulares, você decide em última instância onde definir os limites entre diferentes partes do seu código. Você também pode adotar uma abordagem muito diferente. Em vez de manter a lógica no efeito, você poderia mover a maior parte da lógica imperativa para uma classe JavaScript:

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

Os efeitos permitem conectar o React a sistemas externos. Quanto mais coordenação entre efeitos for necessária (por exemplo, para encadear várias animações), mais faz sentido extrair essa lógica completamente dos efeitos e hooks, como no sandbox anterior. Em seguida, o código que você extraiu se torna o sistema externo. Isso permite que seus efeitos permaneçam simples, pois eles só precisam enviar mensagens para o sistema que você moveu para fora do React.

Os exemplos acima pressupõem que a lógica do fade-in precisa ser escrita em JavaScript. No entanto, essa animação específica de fade-in é mais simples e muito mais eficiente de ser implementada com uma simples Animação CSS.

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Às vezes, você nem precisa de um Hook!

Recap

  • Hooks personalizados permitem compartilhar lógica entre componentes.
  • Hooks personalizados devem ser nomeados começando com use, seguido por uma letra maiúscula.
  • Hooks personalizados compartilham apenas a lógica relacionada ao estado, não o estado em si.
  • É possível passar valores reativos de um Hook para outro, e eles se mantêm atualizados.
  • Todos os Hooks são executados novamente sempre que o componente é renderizado novamente.
  • O código dos seus Hooks personalizados deve ser puro, assim como o código do seu componente.
  • Encapsular manipuladores de eventos recebidos por Hooks personalizados em efeitos de Evento.
  • Não crie Hooks personalizados como useMount. Mantenha o propósito deles específico.
  • Cabe a você escolher como e onde definir os limites do seu código.

Challenge 1 of 5:
Extrair um Hook useCounter

Este componente usa uma variável de estado e um efeito para exibir um número que incrementa a cada segundo. Extraia essa lógica para um Hook personalizado chamado useCounter. Seu objetivo é fazer com que a implementação do componente Counter fique exatamente assim:

export default function Counter() {
const count = useCounter();
return <h1>Segundos que se passaram: {count}</h1>;
}

Você precisará escrever seu Hook personalizado no arquivo useCounter.js e importá-lo no arquivo Counter.js.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Segundos que se passaram: {count}</h1>;
}