Utilizando a Context API eficientemente
26 de maio, 2021 — 4 min read
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.