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.
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
}
// 1 2 3
const [name, setName] = useState(0)
- Valor atual do estado
- Função para alterar o valor do estado (Dispatcher)
- 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 pelosetX
. 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).
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 aceitaasync function() {}
. Para rodar uma async function dentro douseEffect
, 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;
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).
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:
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 cadahandleClick
é 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.
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} />
}
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>
}
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'
})
})