Front End

6 mar, 2019

React: introdução aos Hooks

Publicidade

A nova versão do React (16.8) foi liberada, e o que veio de bom? Finalmente o lançamento dos Hooks! Mas afinal, o que são esses hooks que tanto falam? Os hooks vieram para facilitar nosso desenvolvimento e criação de componentes em React.

A ideia é não precisar mais ficar criando classes e mais classes, fazendo heranças, escrevendo construtores e chamadas super.

Como tudo isso funciona?

Com os hooks não seremos obrigados a criar classes para ter state e componentes statefull, e também não precisaremos de classes para executar determinadas funções em determinado ciclo de vida dos componentes (componentDidMound, componentDidUpdate, etc).

Agora podemos escrever apenas componentes funcionais, e tudo que os componentes de classe tinham, os funcionais também terão.

Chega de conversa e vamos aos hooks. Atualmente (até a data de publicação deste artigo), os hooks oficiais do React, são:

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

Além dos hooks oficiais, também podemos criar os nossos hooks (assunto para um próximo artigo) ou utilizar hooks feitos pela comunidade.

Veremos o que cada um desses hooks faz, quando e porque devemos utilizá-los.

Começando com os hooks

Para que você comece a utilizar os hooks, é necessário que a versão do react e react-dom seja superior ou pelo menos á 16.8.x. Na versão 2.1.5 do create-react-app por padrão ao criar novos projetos as versões dos dois packages (pacotes) já são compatíveis com React Hooks.

Caso a versão que você esteja utilizando seja inferior à 16.8.x, sugiro que atualize.

useState

Usado para adicionarmos estado (state) para nossos componentes funcionais. Para usá-lo temos a seguinte sintaxe:

const [ NOME_DO_ESTADO, FUNCAO_PARA_ATUALIZAR ] = useState(VALOR_INICIAL)

Usamos a sintaxe [NOME_DO_ESTADO, FUNCAO_PARA_ATUALIZAR] para realizar a desconstrução do array de forma simplificada.

Isso porque a função useState devolverá um array com duas posições. A primeira será o valor do estado e a segunda uma função para atualizar o seu valor. Da forma mais longa, ficaria:

const estadoEmArray = useState(VALOR_INICIAL)
const valorDoEstado = estadoEmArray[0]
const funcaoParaAtualizar = estadoEmArray[1]

Portanto, conseguimos criar um componente de contador da seguinte maneira:

const Counter = () => {

    const [ counter, updateCounter ] = useState(0)

    const decrement = () => updateCounter(counter - 1)
    const increment = () => updateCounter(counter + 1)

    return (
        <>
            <h1>Score: { counter }</h1>
            <button onClick={ decrement } type="button">Decrement</button>
            <button onClick={ increment } type="button">Increment</button>
        </>
    )

}

O valor inicial será 0 e temos duas funções: uma para incrementar (increment) e outra para decrementar (decrement), ambas apenas pegam o valor atual do contador e soma ou subtrai mais um.

Caso o próximo valor dependa do valor atual do estado, assim como fazíamos com o setState também podemos passar uma função como parâmetro, onde o primeiro parâmetro será o valor atual. Dessa forma conseguimos realizar as mesmas operações de forma diferente:

const decrement = () => updateCounter(prevCounter => --prevCounter)
const increment = () => updateCounter(prevCounter => ++prevCounter)

Simplesmente recebemos o valor anterior do contador e antes de retorná-lo fazemos adição ou subtração do mesmo.

Observação: diferente do setState tradicional, a atualização de estado com useState não realiza o merge das informações automaticamente. Caso seja necessário, devemos fazer de forma manual e explicita. Vamos ao seguinte exemplo:

const MyName = () => {

    const [ fullName, updateFullName ] = useState({
        name: 'Matheus',
        lastName: null
    })
    const { name, lastName } = fullName

    const updateLastName = () => updateFullName({ lastName: 'Castiglioni' })

    return (
        <>
            <h1>My name is: { `${name} ${lastName}` }</h1>
            <button onClick={ updateLastName } type="button">Update lastName</button>
        </>
    )

}

Ao testar a atualização do sobrenome, podemos ver:

Repare que o valor inicial do sobrenome era null, pois definimos o lastName como null. Mas, ao tentar atualizá-lo, o name recebe o valor de undefined. Isso porque na atualização do sobrenome passamos apenas o lastName, então ao desconstruir o fullName, não existe a chave name, por isso, a variável recebe o valor de undefined.

Como podemos resolver esse problema? Podemos utilizar os recursos de desconstrução:

const updateLastName = () => updateFullName(prevFullName => ({
    ...prevFullName,
    lastName: 'Castiglioni'
}))

Simplesmente desconstruímos o fullName e modificamos o valor de lastName, se realizarmos o teste novamente:

useEffect

Basicamente usamos para substituir o componentDidMount e o componentDidUpdate. O useEffect recebe uma função como parâmetro que será executado em determinadas vezes. Vamos à sintaxe:

useEffect(FUNCAO_PARA_EXECUTAR, [ CAMPOS_OBSERVAVEIS ])

Calma, explicarei os detalhes.

Para chamar a função apenas uma vez, podemos fazer da seguinte maneira:

useEffect(() => {
    console.log('Chamando ao montar')
}, [])

Repare que passamos como segundo parâmetro um array vazio – isso diz para o useEffect ser disparado uma única vez.

Podemos também dizer para o useEffect chamar a função após montar o componente e quando X estado ou propriedade mudar, da seguinte maneira:

useEffect(() => {
    console.log('Chamando a cada mudança do score')
}, [ score ])

No caso, score é um estado interno do componente, mas também poderia ser uma propriedade, ou ambos:

useEffect(() => {
    console.log('Chamando a cada mudança do estado ou propriedade')
}, [ MEU_ESTADO, MINHA_PROPRIEDADE ])

Mas, imagine que realizamos a inscrição em um evento utilizando a biblioteca PubSub:

useEffect(() => {
    PubSub.subscribe(SOME_EVENT)
}, [])

Como podemos dizer para o useEffect remover a inscrição caso o componente seja desmontado (retirado) do DOM? Para isso, podemos retornar uma função que será executada durante esse tempo:

useEffect(() => {
    PubSub.subscribe(SOME_EVENT)
    return () => PubSub.unsubscribe(SOME_EVENT)
}, [])

Seria mais ou menos o que faríamos no componentWillUnmount.

useContext

Usado para facilitar a utilização da Context API, apenas passamos um contexto para o hook como parâmetro e ele devolverá o seu respectivo valor:

const pokemon = useContext(PokemonContext)

O nosso PokemonContext:

const PokemonContext = createContext('Pikachu')

Também poderíamos criar uma função para atualizar o valor do contexto (assunto para um próximo artigo). Assim que esse valor for atualizado, o mesmo será refletido no useContext.

useReducer

No começo do artigo fizemos um contador utilizando apenas o estado interno do componente, mas podemos fazer algo mais avançado. Utilizando o useReducer poderíamos criar um contador da seguinte maneira:

const initialState = {
    counter: 0
}

const reducer = (state, action) => {
    switch (action.type) {
        case 'decrement':
            return { ...state, counter: --state.counter }
        case 'increment':
            return { ...state, counter: ++state.counter }
        default:
            return state
    }
}

const Counter() = () => {

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

    return (
        <>
            <h1>Counter: { state.counter }</h1>
            <button onClick={ () => dispatch({ type: 'decrement' }) } type="button">Decrement</button>
            <button onClick={ () => dispatch({ type: 'increment' }) } type="button">Increment</button>
        </>
    )

}

Temos o mesmo resultado. Alguns motivos para usarmos o useReducer:

  • 1. Quando temos estados com lógicas complexas
  • 2. Necessidade de algum componente filho atualizar o estado, assim não precisamos ficar passando funções de callback – passamos apenas o dispatch, e quando o mesmo for invocado, o reducer será atualizado
  • 3. Novo estado depende do estado anterior

Se você já usou Redux deve ter notado que é bem parecido, mas diferente da convenção do Redux onde fazemos:

const reducer = (state = initialState, action) => {}

Com hooks isso não é usado – isso porque algumas vezes o valor inicial do reducer pode vir de alguma propriedade do componente. Se quiser seguir a convenção do Redux, pode mudar seu useReducer para:

useReducer(reducer, undefined, reducer)

O que não é recomendado.

useCallback

Usado para retornar memoized callbacks. Resumindo, retornará uma função um pouco mais “inteligente”, que só será executada novamente caso um de seus parâmetros mudem. Com isso, conseguimos evitar renderizações desnecessárias em nossos componentes, otimizando a performance da aplicação (similar ao shouldComponentUpdate).

const Counter = () => {

    const [ score, updateScore ] = useState(0)

    const callback = useCallback(() => updateScore(1), [ 1 ])

    return (
        <>
            <h1>Counter: { score }</h1>
            <button onClick={ callback }>Update score</button>
        </>
    )

}

Neste exemplo nosso Counter será atualizado apenas uma vez (na primeira chamada). Depois, como sempre estamos passando o valor 1, o useCallback é inteligente e memorizou esse parâmetro. Assim, enquanto o mesmo não mudar, a função updateScore não será chamada:

Repare que apenas uma vez o valor foi atualizado. Os demais clicks foram ignorados.

useMemo

Similar ao funcionamento do useCallback, a diferença está no retorno:

useCallback(FUNCAO, PARAMETROS) // retorna uma função
useMemo(() => FUNCAO, PARAMETROS) // retorna um valor

Repare que o useMemo retorna uma valor, ao contrário do useCallback, que retorna uma função.

const Counter = () => {

    const memoizedValue = useMemo(() => sum(1, 1), [ 1, 1 ]) // 1 + 1 = 2

    return (
        <>
            <h1>Counter: { memoizedValue }</h1>
        </>
    )

}

useRef

Similar ao createRef, utilizado para referenciarmos elementos ou componentes:

const Title = () => {

    const title = useRef()

    const logTitle = () => console.log(title.current.textContent)

    return (
        <>
            <h1 ref={ title }>My Title</h1>
            <button onClick={ logTitle } type="button">Log title</button>
        </>
    )

}

Repare no exemplo: estamos apenas logando o valor da tag h1. Nossa referência tem uma propriedade chamada .current, que refere-se ao elemento no DOM.

useImperativeHandle

Customiza as propriedades do objeto que retorna para o componente pai quando usamos o ref.

const Title = forwardRef(({ children }, ref) => {

    const title = useRef()

    useImperativeHandle(ref, () => ({
        focus: () => {
            console.log('focando...')
            title.current.focus()
        }
    }))

    return <h1 ref={ title }>{ children }</h1>

})

const Page = () => {

    const title = useRef()

    const logTitle = () => title.current.focus()

    return (
        <>
            <Title ref={ title }>My Title</Title>
            <button onClick={ logTitle } type="button">Log title</button>
        </>
    )

}

Agora, o valor de title.current não será mais uma referência do DOM – será um objeto com a propriedade focus, valor retornado pelo useImperativeHandle.

useLayoutEffect

Funciona do mesmo jeito que o useEffect explicado anteriormente, porém, é disparado apenas quando todas as alterações e mutações no DOM são feitas. Geralmente ele é utilizado para lermos algo no DOM ou fazer renderizações síncronas.

useDebugValue

Usado para logar informações dentro dos nossos custom hooks.

const MyHook = () => {
  useDebugValue('SOME LABEL/INFO')
  return 'My hook'
}

Conclusão

Nesse artigo expliquei um pouco sobre os Hooks do React. Passamos por todos eles (oficiais) e vimos alguns exemplos.

E aí? Já conhecia os hooks? Não deixe de comentar.

Até a próxima!