Skip to content

Instantly share code, notes, and snippets.

@wkrueger
Last active March 4, 2022 21:14
Show Gist options
  • Save wkrueger/067f4fb243fedb98e26ec1c89cb61446 to your computer and use it in GitHub Desktop.
Save wkrueger/067f4fb243fedb98e26ec1c89cb61446 to your computer and use it in GitHub Desktop.

hooks hooks

Instâncias por debaixo dos panos

Um componente React é declarado escrevendo uma função que retorna um conteúdo virtual DOM (JSX) para a biblioteca.

function Cabecalho({ titulo }) {
  return <h1>{titulo}</h1>
}

Componentes não são ausentes de estado (stateless). Por debaixo dos panos o React cria instâncias de componentes e executa ações imperativas sobre esses. Por exemplo, digamos que em um render seja solicitada a emissão da seguinte árvore VDOM:

<Cabecalho titulo="Feijoada" />

Aqui será criada internamente uma instância "Cabecalho 1".

Se no seguinte render isto mude para:

<Cabecalho titulo="Polenta" />

... a mesma instância "Cabeçalho 1" será reutilizada para calcular o resultado.

Caso no próximo render o conteúdo mude para:

<Imagem fonte="banana.jpg" />

... então a instância "Cabecalho 1" será destruída, e uma nova instância "Imagem 1" será criada.

Regras dos hooks

O React disponibiliza as seguintes funções chamadas de "hooks":

  • useState
  • useReducer
  • useEffect
  • useRef
  • useMemo
  • useCallback
  • useContext
  • e mais...

Algumas regras são impostas quando ao uso de hooks:

  • O nome de uma função hook sempre comaça com "use";
  • Eles só podem ser chamados dentro de componentes (funções declaradoras de componentes);
  • Todo "render" de componente deve chamar sempre os mesmos hooks na mesma ordem. Ou seja, hooks não podem estar dentro de "if"s ou após retornos condicionais.

Como cada chamada de hook dentro de um componente não possui um identificador, o identificador é a própria ordem.

Dado o seguinte componente:

function Counter() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')
  return <p>
    {count} - {name}
  </p>
}

Imagina-se que, por baixo dos panos, o React criará internamente uma instância com uma cara mais ou menos assim.

// CONTADOR 1
{
  hooks: [
    { type: 'useState', value: 0 },
    { type: 'useState', value: '' },
  ],
  render: Contador
}

Hooks mais populares

useState

//      1       2                3
const [name, setName] = useState(0)
  1. Valor atual do estado
  2. Função para alterar o valor do estado (Dispatcher)
  3. Valor inicial do estado, lido no primeiro render
function Sample() {
  const [name, setName] = useState(0)
  return <div>
    <button onClick={() => setName('jude')}>
      btn1
    </button>
  </div>
}

Considerações

  • O primeiro retorno de useState nunca deve ser diretamente mutado. As alterações sempre devem ser efetuadas pelo setX. Alterações de objetos devem ser feitas "de forma imutável". Mais informações.
const [person, setPerson] = useState({ name: 'Hitch', age: 20 })
function handleClick(){
  person = { name: 'Nisha' } // <-- ❌ NÃO PODE FAZER ISSO
  person.name = 'Paul' // <-- ❌ NÃO PODE FAZER ISSO
  setPerson(old => {
    old.name = 'Marcos' // <-- ❌ NÃO PODE FAZER ISSO
    return old
  })
  setPerson({ ...person, name: 'Khalid' }) // <-- ✔️ OK
  setPerson(old => { ...old, name: 'Khalid' }) // <-- ✔️ OK
}
  • Um setState sempre invocará um novo render do componente.
  • O setState aceita 2 formas de chamada. Diretamente passando o novo valor, ou utilizando um callback:
setPerson({ ...person, name: 'Anna' })
setPerson(old => {
  return { ...old, name: 'Anna' }
})
  • A forma de callback é útil usada em conjunção com hooks que possuem listas de dependências (mais adiante veremos).

useEffect

Prêmio de design de API mais confusa do React.

Roda um callback, APÓS o render:

  • Sempre na primeira vez em que o componente é executado;
  • Sempre que uma das dependências passadas sofrerem alteração;

Assinatura:

type UseEffectCallback = () => (void | (() => void))
useEffect(cb: UseEffectCallback, dependencies?: any[]): void

Exemplo 1:

import { useHandleError, fetchEntity } from './my-code'

function Component({ id }) {
  const [entity, setEntity] = useState<null | Entity>(null)
  const handleError = useHandleError()
  useEffect(() => {
    async function runEffect() {
      try {
        const result = await fetchEntity(id)
        setEntity(result)
      } catch (err) {
        handleError(err)
      }
    }
    runEffect()
  }, [id]) // <-- efeito roda quando `id` muda
  if (!entity) return null;
  return <p>{entity.name}</p>
}

Ainda, o useEffect aceita opcionalmente uma função de "limpeza" (mais raramente usada), que é executada nas seguintes condições:

  • Antes da execução do próximo callback do useEffect (note que não é rodada antes da primeira execução);
  • Quando o componente é destruído;

A função de limpeza é especificada como retorno do callback do useEffect (exemplo 2):

// doc oficial react
useEffect(() => {
  // chamemos isso de FUNÇÃO DO EFEITO
  const subscription = props.source.subscribe();
  return () => {
    // chamemos isso de FUNÇÃO DE LIMPEZA
    subscription.unsubscribe();
  };
}, [props.trigger]);

Por causa do formato da função de limpeza, o useEffect NÃO aceita async function() {}. Para rodar uma async function dentro do useEffect, ela tem que ser declarada separadamente (como no exemplo 1) ou usando uma IIFE.

Seguindo o exemplo 2, a seguinte sequência de eventos ocorreria:

primeira execução do componente
componente recebe prop { trigger: 1 }
render
"função do efeito 1" é executada
componente recebe prop { trigger: 1 }
render
efeito não é executado pois dependência não mudou
componente recebe prop { trigger: 2 }
render
"função de limpeza 1" é executada
"função do efeito 2" é executada
componente será destruído
"função de limpeza 2" é executada
componente destruído

Ainda, dois casos específicos relacionados à lista de dependências merecem atenção:

  • Quando a lista de dependências é um array vazio, o efeito apenas irá executar uma vez, após o primeiro render do componente. A função de limpeza só é executada uma vez na destruição do componente;
  • Quando a lista de dependências é ausente, o useEffect será executado em todo render;

Caso de uso: Buscando informações assíncronas

O caso de uso mais comum do useEffect é o carregamento de informações recebidas de um meio assíncrono. O exemplo 1 repetido aqui representa este caso:

import { useHandleError, fetchEntity } from './my-code'

function Component({ id }) {
  const [entity, setEntity] = useState<null | Entity>(null)
  const handleError = useHandleError()
  useEffect(() => {
    async function runEffect() {
      try {
        const result = await fetchEntity(id)
        setEntity(result)
      } catch (err) {
        handleError(err)
      }
    }
    runEffect()
  }, [id]) // <-- efeito roda quando `id` muda
  if (!entity) return null;
  return <p>{entity.name}</p>
}

No exemplo acima, sempre que id muda, entity deve ser atualizado para refletir. Temos uma relação de dependência entre essas variáveis.

A relação de dependência assíncrona geralmente é expressa a partir de um par de hooks: um useState para receber o valor dependente e um useEffect pra realizar a atualização.

Considerações avançadas

A existência de uma operação assíncrona trás à tona uma variável "oculta" - o estado da requisição. Completo, pendente ou com falha. Se quiséssemos representar mais corretamente o tipo de entity, poderíamos ter usado algo como:

type AsyncData<Entity> = 
  | { status: 'RESOLVED', data: Entity  }
  | { status: 'PENDING' }
  | { status: 'ERROR', error: any }

Ao invés disso estamos usando Entity | null, que é uma simplificação (quebra-galho) onde nulo representa que o dado ainda não foi carregado. Reconhecer estes estados intermediários através de tipos é importante pra evitarmos erros e até pra tratarmos estados de carregamento (exemplo, exibir um spinner enquanto a informação não carregou).

Informações síncronas

Lidando com dependências

Regra geral de imutabilidade do React

Múltiplas rotinas no React dependem de comparações === entre valores para determinar se houve alteração entre um estado anterior e um estado atual. Dado isso, estabelecemos que:

  • Se um objeto (funções são objetos) não sofreu alteração entre renders, ele deve manter a mesma referência;
  • Se um objeto sofreu alteração entre renders, ele deve ter sua referência modificada;

Exemplo:

ANTES
{ //1, versão 1
  name: { first: "Julia", last: "Roberts" }, //2, versão 1
  address: { line1: "Street" } //3, versão 1
}

DEPOIS
{ //1, v2
  name: { first: "Julia", last: "Roberts" }, //2, v1
  address: { line1: "Avenue" } //3, v2
}
  • O objeto 3 (address) sofreu alteração e terá uma nova referência;
  • Consequentemente, o item 1 também, por cascata, terá nova referência;
  • O objeto 2 (name) não sofreu alteração e deverá manter a mesma referência

Seguimos com alguns exemplos que fazem uso da comparação estrita:

Atualizações da React DOM

function Sample() {
  function handleClick() {
    console.log('click')
  }
  return <>
    <button id="btn1">btn1</button>
    <button id="btn2" onClick={handleClick}>btn2</button>
  <>
}

No exemplo acima, espera-se que a cada render:

  • O botão #btn1 não seja modificado na DOM pelo React;
  • O botão #btn2 será alterado em todo render, pois a referência de cada handleClick é diferente;
// internamente, algo assim acontecerá em todo render
buttonElement.onclick = newHandleClick

Para resolver esse problema, usa-se o hook useMemo (ou useCallback, que é praticamente a mesma coisa).

function Sample() {
  const handleClick = useCallback(() => {
    console.log('click')
  }, [])
  // ou
  const handleClick = useMemo(() => () => {
    console.log('click')
  }, [])
  return <>
    <button id="btn1">btn1</button>
    <button id="btn2" onClick={handleClick}>btn2</button>
  <>
}

Veja que aqui a função primária da memoização não é economizar cálculos na definição da função, mas sim fazer com que ela tenha a mesma referência quando ela não é alterada.

memo

A função memo() cria um novo componente onde é realizada uma checagem de igualdade entre as props anteriores e atuais. Caso não hava modificações, o render desse componente é pulado.

function Card({ title, content }) {
  return //...
}
const CardMemo = memo(Card)

function Consumer() {
  // ...
  return <CardMemo title={title} content={content} />
}

useEffect

O useEffect é executado depois do render SE uma das suas dependências (do seu segundo argumento) tiver sido alterada. Desta forma, é importante que dependências que são objetos ou funções sigam a regra de imutabilidade, ou o efeito sempre será executado. Mais detalhes em seção específica.

function Component({ id }) {
  const [entity, setEntity] = useState<null | Entity>(null)
  useEffect(() => {
    async function runEffect() {
      try {
        const result = fetchEntity(id)
        setEntity(result)
      } catch (err) {
        handleError(err)
      }
    }
    runEffect()
  }, [id])
  if (!entity) return null;
  return <p>{entity.name}</p>
}

Ferramenta útil: immer

Pode ser extremamente tedioso realizar alterações imutáveis em posições aninhadas de objetos e listas;

setState(previous => {
  return {
    ...previous,
    address: {
      ...previous.address,
      name: 'new name',
    }
  }
})

A biblioteca "immer" é feita para ajudar nesse caso de uso. A sua função é "criar o um novo estado imutavel a partir de operações mutáveis".

import immer from 'immer'

setState(previous => {
  return immer(previous, draft => {
    draft.address.name = 'new name'
  })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment