Utilizando a Context API eficientemente

26 de maio, 2021 — 4 min read

Utilizando a Context API eficientemente

O que é Context API?

A Context API é uma forma de compartilhar dados entre componentes em diferentes níveis de uma árvore de elementos, muito utilizado para evitar prop drilling e até mesmo como um gerenciador de estados.

O último caso que comentei sobre utilizar Context como um gerenciador de estados é polêmico, pois por muito tempo Redux, MobX e outros dominavam essa área e utilizamos esses gerenciadores mesmo não precisando, mas com certeza uma coisa que devemos tomar cuidado em utilizar a Context API como gerenciador de estados é que quando houver alterações em qualquer dado dele vai acontecer uma nova renderização, mesmo não utilizando o dado alterado especificamente.

Mais sobre a Context API

Primeiramente dependendo de quando estiver lendo este post, recomendo olhar essa Pull Request no repositório do React, onde está sendo implementado uma forma de consumir somente um dado do contexto, também recomendo esses artigos do mestre Kent C. Dodds de como organizar um arquivo de Context e como otimizá-lo.

Como utilizar eficientemente

A minha definição de eficiente é, com uma API boa de se utilizar e que tenha boa performance independente do tamanho da aplicação.

import { createContext, ReactNode, useCallback, useState } from 'react'

interface CounterContextType {
  counter: number
  increment: () => void
  decrement: () => void
}

const counterContext = createContext<CounterContextType>(
  {} as CounterContextType
)

const CounterProvider = ({ children }: { children: ReactNode }) => {
  const [counter, setCounter] = useState(0)

  const increment = useCallback(() => {
    setCounter((c) => c + 1)
  }, [])

  const decrement = useCallback(() => {
    setCounter((c) => c - 1)
  }, [])

  return (
    <counterContext.Provider
      value={{
        counter,
        increment,
        decrement,
      }}
    >
      {children}
    </counterContext.Provider>
  )
}

export { CounterProvider, counterContext }

Normalmente definimos um contexto dessa forma, vou utilizar um exemplo de contador bem simples, mas imaginemos algo em escala maior.

Para acompanhar as renderizações vou criar um hook e utilizar nos componentes

import { useRef } from 'react'

export const useRenderCounter = (componentName: string) => {
  const renderCount = useRef(0)
  renderCount.current = renderCount.current + 1

  return (
    <>
      <h3>{`${componentName} have: ${renderCount.current} renders`}</h3>
      <hr />
    </>
  )
}

Para vermos as novas renderizações acontecendo vou separar em três componentes que utilizam esse contexto acima.

import { useContext } from 'react'

import { useRenderCounter } from '../hooks/useRenderCounter'
import { counterContext } from './ContextDefault'

export const ShowCounter = () => {
  const renderCount = useRenderCounter('ShowCounter')

  const { counter } = useContext(counterContext)

  return (
    <>
      <h2>Counter: {counter}</h2>
      {renderCount}
    </>
  )
}
// ...same imports

export const IncrementCounter = () => {
  const renderCount = useRenderCounter('IncrementCounter')

  const { increment } = useContext(counterContext)

  return (
    <>
      <button type="button" onClick={increment}>
        Increment
      </button>
      {renderCount}
    </>
  )
}
// ...same imports

export const DecrementCounter = () => {
  const renderCount = useRenderCounter('DecrementCounter')

  const { decrement } = useContext(counterContext)

  return (
    <>
      <button type="button" onClick={decrement}>
        Decrement
      </button>
      {renderCount}
    </>
  )
}

Com os componentes criados podemos centralizá-los e utilizar o Provider criado no arquivo de contexto.

import { CounterProvider } from './ContextDefault'

import { ShowCounter } from './ShowCounter'
import { IncrementCounter } from './IncrementCounter'
import { DecrementCounter } from './DecrementCounter'

export const MainDefaultContext = () => {
  return (
    <CounterProvider>
      <h1>Default Context</h1>
      <ShowCounter />
      <IncrementCounter />
      <DecrementCounter />
    </CounterProvider>
  )
}

Testando:

Você deve ter percebido que todos os componentes são renderizados quando o counter é atualizado, quando somente o componente ShowCounter utiliza o valor.

Nesse caso podemos utilizar a otimização recomendada pelo Kent ou a biblioteca que quero introduzir nesse artigo, a use-context-selector, a otimização citada pelo Kent de separar o valor do handler que atualiza esse valor em diferentes contextos funciona em grande parte dos casos, mas para utilizar a Context API de uma forma mais completa com uma API limpa, essa biblioteca é sensacional.

Utilizando use-context-selector

A declaração do contexto não é muito diferente, só muda que utilizamos a função fornecida pela biblioteca.

import { ReactNode, useCallback, useState } from 'react'
import { createContext } from 'use-context-selector'

interface CounterContextType {
  counter: number
  increment: () => void
  decrement: () => void
}

const counterContext = createContext<CounterContextType>(
  {} as CounterContextType
)

const CounterProvider = ({ children }: { children: ReactNode }) => {
  const [counter, setCounter] = useState(0)

  const increment = useCallback(() => {
    setCounter((c) => c + 1)
  }, [])

  const decrement = useCallback(() => {
    setCounter((c) => c - 1)
  }, [])

  return (
    <counterContext.Provider
      value={{
        counter,
        increment,
        decrement,
      }}
    >
      {children}
    </counterContext.Provider>
  )
}

export { CounterProvider, counterContext }

Agora a maior diferença fica na hora de utilizarmos o Context criado, ainda podemos consumir o contexto com o hook useContext fornecido pela biblioteca, mas o motivo de estarmos utilizando ela é o hook useContextSelector.

import { useContextSelector } from 'use-context-selector'

import { counterContext } from './ContextSelector'
import { useRenderCounter } from '../hooks/useRenderCounter'

export const ShowCounter = () => {
  const renderCount = useRenderCounter('ShowCounter')

  const counter = useContextSelector(counterContext, ({ counter }) => counter)

  return (
    <>
      <h2>Counter: {counter}</h2>
      {renderCount}
    </>
  )
}

Primeiro passamos o contexto que será utilizado e como segundo parâmetro do hook um handler, nesse handler o parâmetro que recebemos é o valor do contexto em si, então posso desestruturar, fazer qualquer tratativa e então retornar o valor que eu quiser para o componente com base nos valores do contexto, nesse caso peguei o counter e só retornei o valor.

Exemplo de alterações que podem ser feitas pensando que existe um carrinho de compras no contexto:

const cartInfos = useContextSelector(myContext, ({ cart }) => {
  return {
    cart,
    cartAmount: cart.length,
    cartIsEmpty: !cart.length,
    isHavePotatoOnCart: cart.some((product) => product.name === 'Potato'),
  }
})

Por conta desse hook e os recursos fornecidos acho muito interessante utilizar essa biblioteca quando pensamos em escalar uma aplicação utilizando a Context API.

Agora voltando ao exemplo, os outros componentes de incrementar e decrementar o counter serão iguais aos anteriores alterando só a forma com que consumimos o valor do contexto.

// ...same imports

export const IncrementCounter = () => {
  const renderCount = useRenderCounter('IncrementCounter')

  const increment = useContextSelector(
    counterContext,
    ({ increment }) => increment
  )

  return (
    <>
      <button type="button" onClick={increment}>
        Increment
      </button>
      {renderCount}
    </>
  )
}
// ...same imports

export const DecrementCounter = () => {
  const renderCount = useRenderCounter('DecrementCounter')

  const decrement = useContextSelector(
    counterContext,
    ({ decrement }) => decrement
  )

  return (
    <>
      <button type="button" onClick={decrement}>
        Decrement
      </button>
      {renderCount}
    </>
  )
}

E agora hora de testar, será que esse hook fornece somente uma forma mais detalhada de consumirmos os valores de um contexto ou realmente interfere na performance?

Somente os componentes que utilizam os valores que são atualizados sofrem novas renderizações!

Versão final com os dois exemplos:

Conclusão

Inicialmente a Context API não era recomendada para gerenciar os estados de grandes aplicações, mas olhando o futuro daquela Pull Request, as otimizações citadas pelo Kent e a biblioteca que mostrei, é sim possível organizar contextos e escalar uma aplicação utilizando só a Context API para gerenciar esses estados.

E para finalizar, não sou contra nenhuma outra biblioteca para gerenciar estados, ultimamente tenho olhado bastante para Recoil e jotai, ambas resolvem essa questão de gerenciar estados de uma forma limpa, com uma API enchuta e boa de se utilizar, caso você utilize GraphQL e como client o Apollo também recomendo olhar as Reactive Variables, combinado com o cache é simplesmente poderoso.


Se inscreva para novidades:

Para mais informações, acesse:Política de Privacidade

Enviar
A. Zagatti Logo

André Zagatti

Engenheiro de software frontend que gosta de compartilhar conhecimento.

© 2023, André Zagatti