Context API: saiba como simplificar o gerenciamento de estados no React

Conteúdos deste artigo:

Gerenciar dados em aplicações pode ser um grande desafio – principalmente quando muitos componentes estão envolvidos no compartilhamento e atualização desses dados. Os componentes do React usam states para gerenciar propriedades internamente, geralmente sendo manipulados usando hooks do React, como useState ou useEffect. No entanto, as coisas podem ficar um pouco complicadas quando devemos passar essas propriedades entre diversos componentes. É importante ter em mente que lidar com esses estados em grandes projetos é uma tarefa complicada, gerando a impressão de que é quase obrigatório usar bibliotecas de terceiros para ajudar nessa tarefa. É exatamente aí que a Context API é útil.

O que é Context API e qual a sua utilidade?

Em primeiro lugar, Context é uma API incorporada no React 16.3 que ajuda os desenvolvedores a evitar a passagem de dados por meio de uma cadeia de props. Em vez disso, a solução torna possível passar dados de componentes pais para filhos aninhados diretamente – algo semelhante ao Redux, mas de forma mais leve e fácil.

Além de ser uma forma de criar variáveis globais que podem ser chamadas sob demanda, Context API é uma ótima alternativa para evitar prop drilling. Também é muito simples de usar: basta criar um contexto usando React.createContext(), retornando um Consumer e um Provider. Entenda mais sobre cada um destes termos:

  • Um Provider, fornece estados para seus componentes filhos, tornando-se o pai de todos os componentes que podem precisar desses estados;
  • Já um Consumer é o componente que consome os estados.

Para obter uma compreensão real do conceito, o projeto abaixo exemplifica algumas das aplicações da Context API. Ele consiste em um contador de pontuação simples para duas equipes e um card de resultados, que mostra a pontuação total de cada equipe.

Aplicação de exemplo

Nesta aplicação, precisamos gerenciar dois estados, um para cada uma das pontuações das equipes. Fora isso, as funções onClick irão lidar com esses estados e retornar a soma dos pontos. Temos duas maneiras de operar esses estados: prop drilling e Context API. É importante conhecer ambas para, assim, ter uma noção real dos prós e contras e quando usar uma ou outra.

Prop drilling: o que é?

Com a estratégia de prop drilling, os estados necessários para os componentes são passados como propriedades pelos próprios componentes. Se você ainda está confuso, confira o funcionamento do processo no fluxograma abaixo:

Fluxograma da abordagem prop drilling

Aqui, os estados são criados na página inicial e enviados, via props, para os componentes Counter e Result. Eles, por sua vez, enviam esses estados para outros componentes que se aprofundam cada vez mais em sua aplicação. Isso causa o efeito “drill” e cria componentes aninhados.

Confira como nosso componente Home está estruturado, inicializando estados e renderizando os componentes filhos, Counter e Result:

export default function Home() {
  const [teamA, setTeamA] = useState(0);
  const [teamB, setTeamB] = useState(0);

  return (
    <Container>
      <Box>
        <Counter
          teamA={teamA}
          setTeamA={setTeamA}
          teamB={teamB}
          setTeamB={setTeamB}
        />
      </Box>
      <Result result={teamA + teamB} />
    </Container>
  );
}

Qual o problema do prop drilling?

Como mencionado anteriormente, a utilização de um contexto nos ajuda a evitar o prop drilling. Mas por que é ruim usar o prop drilling e quais problemas essa estratégia pode causar? 

No nosso exemplo da aplicação, há poucos problemas em utilizar essa abordagem, já que nossa aplicação ainda é pequena e possui uma quantidade baixa de componentes e interações de dados entre eles. Mas, e se tivéssemos um sistema mais complexo e com mais componentes para administrar?

Certamente, este não seria apenas um sistema confuso, mas que também causaria uma dor de cabeça em quem precisasse realizar manutenções e futuras implementações.

Na verdade, nem precisaríamos de um sistema maior para enfrentar problemas: se você simplesmente quiser apenas renomear o estado prop com o qual está trabalhando, o prop drilling fará com que você vasculhe cada arquivo e renomeie cada chamada de estado individualmente.

Na verdade, nem precisaríamos de um sistema maior para enfrentar problemas: se você simplesmente quiser apenas renomear o estado com o qual está trabalhando, o prop drilling fará com que você tenha que vasculhar cada arquivo e renomear cada chamada de estado individualmente.

E, apesar de ser muito simples, a abordagem de prop drilling vai forçá-lo a passar dados do componente pai para níveis abaixo até chegar ao filho desejado, mesmo que você não precise dos dados nesses componentes de nível superior. Isso torna o desenvolvimento desnecessariamente complexo.

Como um dos pilares do time de Sistemas da BIX Tecnologia envolve justamente a entrega de códigos performáticos e de fácil manutenção até mesmo por terceiros, o prop drilling não tem vez por aqui. E, por isso, na maioria dos casos, a escolha do nosso time está na Context API.

Como usar Context API?

Nosso objetivo aqui é fornecer dados para a aplicação sempre que necessário. Portanto, em vez de passar os dados para o componente de nível superior, vamos fornecer os dados sob demanda apenas onde são necessários.

Assim, para implementarmos isso, vamos utilizar o Provider Pattern. Ele nada mais é do que um HOC (Componente de Ordem Superior) fornecido por um objeto de contexto. Esse provider receberá uma propriedade de valor que contém os dados que queremos transmitir aos componentes. Cada componente agrupado nesse provider terá acesso ao valor.

E como utilizar dados em cada componente?

Já que estamos no React, hooks são o caminho a seguir: importe o hook useContext, que recebe o contexto que contém a propriedade value que criamos. Isso nos permitirá ler e gravar dados no contexto.

Ao fazer isso, os componentes que não estão usando os dados não terão que lidar com eles, e os dados serão chamados apenas quando e onde necessário, no escopo correto.

Exemplos práticos da abordagem

Vamos voltar ao exemplo da nossa aplicação do placar. Primeiro, construímos um App Provider. Ele vai envolver a raiz da nossa aplicaçãono componente App Provider.

Como você deve ter percebido, não é neste context que a Context API funciona de verdade, certo? Podemos, e às vezes devemos, ter mais de um contexto em nossas aplicações. Isso ajudará a criar uma solução modular, facilitando o desenvolvimento e a manutenção posteriores.

App

import AppProvider from "./contexts";
import Home from "./pages/Home";

function App() {
  return (
    <AppProvider>
        <Home />
    </AppProvider>
  );
}

export default App;

AppProvider

import React from 'react'

import { ScoreProvider } from './ScoreContext'

const AppProvider = ({ children }) => (
      <ScoreProvider>{children}</ScoreProvider>
)

export default AppProvider

Após isso, utilizando createContext(), seremos capazes de criar o ScoreProvider. Esse é o elemento-chave da abordagem Context API.

Aqui, vamos declarar todos os estados de que precisamos, aplicar handlers como funções e, claro, exportar tudo como um objeto para que possamos importar props individuais em cada componente.

ScoreProvider

import React, {
  createContext,
  useState,
} from 'react'

export const ScoreContext = createContext({})

const ScoreProvider = ({ children }) => {
  const [teamA, setTeamA] = useState(0);
  const [teamB, setTeamB] = useState(0);

  function handleIncreaseTeamAScore(){
      setTeamA((prevScore) => prevScore+1)
  }

  function handleIncreaseTeamBScore(){
      setTeamB((prevScore) => prevScore+1)
  }

  function handleDecreaseTeamAScore(){
    setTeamA((prevScore) => prevScore-1)
}

  function handleDecreaseTeamBScore(){
      setTeamB((prevScore) => prevScore-1)
  }

  function getScoreSum(){
      return teamA+teamB
  }

  return (
    <ScoreContext.Provider
      value={{
        teamA,
        setTeamA,
        teamB,
        setTeamB,
        handleIncreaseTeamAScore,
        handleIncreaseTeamBScore,
        handleDecreaseTeamAScore,
        handleDecreaseTeamBScore,
        getScoreSum
      }}
    >
      {children}
    </ScoreContext.Provider>
  )
}

export { ScoreProvider }

Outra atitude inteligente a tomar nessa abordagem é criar um hook que faça a chamada do contexto por nós. Isso ajuda muito na construção de um código limpo e modular.

useScore hook

import { useContext } from "react"
import { ScoreContext } from "../contexts/ScoreContext"

function useScore() {
    const context = useContext(ScoreContext)

    if (!context) {
      throw new Error('useScore must be used within a ScoreProvider')
    }

    return context
  }

  export { useScore }

Tudo isso é feito para que, no final, nossos componentes possam evoluir do que vimos anteriormente, para o exemplo abaixo. Observe como apenas chamamos o que precisamos em cada um deles.

Home Page

export default function Home() {
  return (
    <Container>
      <Box>
        <Counter />
      </Box>
      <Result />
    </Container>
  );
}

Resultados

export function Result() {
  const { getScoreSum } = useScore();
  return (
    <Box>
      <Paper>
        <h1>Sum of score</h1>
        <h2>{getScoreSum()}</h2>
      </Paper>
    </Box>
  );
}

Contador

export function Counter() {
  return (
    <Box>
      <TeamA />
      <TeamB />
    </Box>
  );
}

Componente Equipe A

export function TeamA() {
  const { teamA, handleIncreaseTeamAScore, handleDecreaseTeamAScore } =
    useScore();

  return (
    <Paper>
      <Box>
        <h2>Team A</h2>
      </Box>
      <Scoreboard
        score={teamA}
        handleIncreaseScore={handleIncreaseTeamAScore}
        handleDecreaseScore={handleDecreaseTeamAScore}
      />
    </Paper>
  );
}

Componente do Placar

export function Scoreboard({
  score,
  handleIncreaseScore,
  handleDecreaseScore,
}) {
  return (
    <Paper>
      <h2>Score Board</h2>
      <Stack>
        <Button onClick={handleIncreaseScore}>Score +</Button>
        <Button onClick={handleDecreaseScore}>Score - </Button>
      </Stack>
      <h2>{score}</h2>
    </Paper>
  );
}

Quais conclusões tirar sobre a Context API?

É importante mencionar que também existem alternativas para a Context API do React. Tudo pode depender do tamanho da sua aplicação, dependências e complexidade, mas alternativas como Redux, Hookstate ou Recoil são muito populares e também devem ser consideradas. Cada um deles terá seus prós e contras. No entanto, nosso time de Sistemas entende que a Context API funciona muito bem na maioria dos casos.

A Context API do React estabelece a base para evitar o prop drilling e, depois que você começa a usá-la, é difícil voltar ao que você utilizava antes. Por isso, recomendamos que você a experimente em seus projetos, encontre suas limitações e explore outras opções.

E, se precisar de ajuda, conte com o time de especialistas em Sistemas da BIX Tecnologia! Clique no banner abaixo e entre em contato conosco.