Análise comparativa: Context API vs Redux

Algo muito comum para nós na área de desenvolvimento é querer comparar duas tecnologias, por exemplo: React com classes vs React com hooks, useReducer vs useState, o nosso tema de hoje Context API vs Redux, e várias outras.

Como dito antes, hoje iremos focar em comparar a Context API com o Redux, se uma é melhor do que a outra, onde usar uma e onde usar a outra, elas realmente servem para a mesma coisa? Entre outras comparações.

Comparação 01: O que são

Primeiro vamos entender o que cada uma das duas é, e começando pela Context API, ela é um módulo do React que serve para podermos compartilhar dependencias ao longo da árvore de componentes sem termos que passa-lás na forma de props.

O Redux por outro lado é uma biblioteca criada para facilitar a implementação do padrão flux, que é um padrão de arquitetura desenvolvido pelo time do facebook, então ele entra na categoria de bibliotecas de gerenciamento de estado, sendo sua principal funcionalidade ajudar a controlar manipulações de estado muito complexas ao longo da aplicação.

Comparação 02: Quais as similaridades

Bom, se estamos comparando esses dois, deve ser porque eles tem algo em comum certo? Então é isso que vamos discutir agora. Como vimos na seção “O que são”, eles não são a mesma coisa (na verdade são coisas muito diferentes), o que faz as pessoas compararem esses dois, é o fato de geralmente utilizarmos a Context API (e o próprio Redux também) para implementarmos o padrão flux na nossa aplicação.

Um dos 3 pílares do padrão Flux é a store (que é onde nós centralizamos todo o estado da aplicação), por isso precisamos guardar todo o estado em um objeto que vai lidar com as mutações nele, e ai distribuímos esse estado que foi centralizado na store para o resto da aplicação.

No React o jeito que nós temos para poder fazer essa distribuição é justamente a Context API.

Além disso, um dos usos mais comuns para a Context API é implementar um sistema de gerenciamento de estado bem básico, geralmente usando o padrão Flux mesmo, e é isso que causa essa comparação entre os dois.

Comparação 03: Instalação

Para podermos utilizar a Context API só precisamos do React instalado, enquanto para o Redux precisariamos de outros pacotes como o redux-toolkit e o react-redux. Então considerando que vamos utilizar React, a Context API se demonstra bem mais prática nesse caso, afinal uma dependência a menos no projeto é sempre bem-vinda.

Porém uma observação que não pode faltar é que o Redux não é uma lib feita para o React (por isso que ele tem um pacote react-redux), o que significa que podemos utilizar ele fora do React com outros frameworks/libs ou até mesmo com JS puro, e no caso do JS puro ele seria mais prático por só ter que instalar o redux-toolkit (que já viria com o Redux junto).

Comparação 04: Setup

Para criarmos um estado centralizado, e seguindo o Flux, usando a Context API, nós podemos fazer algo mais ou menos assim:

import { createContext, ReactNode, useReducer } from "react";
import counterReducer, { CounterState } from "../../modules/counter/reducer";

export type AppState = CounterState;

type AppProviderProps = { children: ReactNode };

const initialState: AppState = { count: 0 };
const initialContextState = {
  ...initialState,
  dispatch: (() => null) as React.Dispatch<{ type: string }>,
};
export const AppContext = createContext(initialContextState);

export type AppContextState = typeof initialContextState;

function AppProvider({ children }: AppProviderProps) {
  const [store, dispatch] = useReducer(counterReducer, initialState);

  return (
    <AppContext.Provider value={{ ...store, dispatch }}>
      {children}
    </AppContext.Provider>
  );
}

export default AppProvider;

E para o counterReducer, seria algo como:

export type CounterState = { count: number };

export function counterReducer(state: CounterState, action: { type: string }) {
  return (
    {
      INCREMENT: { ...state, count: state.count + 1 },
    }[action.type] ?? state
  );
}

export const increment = () => ({ type: "INCREMENT" })

export default counterReducer;

Enquanto no Redux seria assim:

import { configureStore } from "@reduxjs/toolkit";
import counter from "../../modules/counter/slice";

const store = configureStore({
  reducer: {
    counter,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

E o reducer seria:

import { createSlice } from "@reduxjs/toolkit";
import type { RootState } from "../../app/store/redux";

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
  },
});

export const { increment } = counterSlice.actions;
export const selectCount = (state: RootState) => state.counter.value;
export default counterSlice.reducer;

Antigamente a versão Redux gastava bem mais código, além de parecer muito com a versão context, mas nas versões mais atuais tudo se tornou bem mais enxuto graças ao redux toolkit, que abstraiu vários desses padrões que surgiram a partir do uso do redux puro em uma biblioteca só.

Comparação 05: Hooks auxiliares

É interessante, principalmente quando estamos usando TypeScript, criar alguns hooks auxiliares para lidar com o uso do useContext ou do useSelector. Então vamos criar um hook para tipar no o nosso useContext:

import { useContext } from "react";
import { AppContext, AppContextState } from "../store";

export function useAppStore() {
  return useContext<AppContextState>(AppContext);
}

export default useAppStore;

E agora vamos criar os hooks para tipar o useDispatch e o useSelector do Redux:

import { useDispatch } from "react-redux";
import type { AppDispatch } from "../store/redux";

export const useReduxDispatch = () => useDispatch<AppDispatch>();
export default useReduxDispatch;
import { TypedUseSelectorHook, useSelector } from "react-redux";
import type { RootState } from "../store/redux";

export const useReduxSelector: TypedUseSelectorHook<RootState> = useSelector;
export default useReduxSelector;

Comparação 06: Uso

E agora que temos tudo pronto, vamos ver como usariamos cada um em um componente:

import { useAppStore } from "../../../app/hooks";
import { increment } from "../reducer";

export function ContextCounter() {
  const { dispatch, count } = useAppStore();
  const dispatchIncrement = () => dispatch(increment());

  return (
    <button className="d:ib w:auto" onClick={dispatchIncrement}>
      {count}
    </button>
  );
}

export default ContextCounter;
import { useReduxDispatch, useReduxSelector } from "../../../app/hooks";
import { increment, selectCount } from "../slice";

export function ReduxCounter() {
  const dispatch = useReduxDispatch();
  const count = useReduxSelector(selectCount);
  const dispatchIncrement = () => dispatch(increment());

  return (
    <button className="d:ib w:auto" onClick={dispatchIncrement}>
      {count}
    </button>
  );
}

export default ReduxCounter;

Perceba que na hora de utilizar de fato não há muita diferença, afinal a ideia do padrão Flux é só deixar a passagem de mensagens para o componente e todo o resto deve ser feito em um reducer, por isso o código no componente fica bem mínimo.

Porém o Redux leva a melhor aqui, pois ele nos permite passar uma função como parâmetro do useSelector que nos permite retirar só um pedaço do estado dentro da store, ou rececer um estado já derivado (por exemplo se estivessemos criando uma lista com um filtro, poderiamos já pegar a lista filtrada direto do useSelector). Claro, temos como fazer o mesmo com useContext (alterando um pouco o nosso hook useAppStore por exemplo), mas ai teríamos que programar mais coisas, enquanto o Redux já nos dá isso pronto.

Comparação 07: Performance

O Redux já otimiza as rendenizações dos componentes por padrão, enquanto com a Context API precisamos otimizar os componentes através do React.memo para evitar que uma alteração em algum estado da store cause a re-rendenização de toda a árvore que depende da store (mesmo que ela não dependa daquele estado em específico que foi atualizado).

Comparação 08: Middlewares e Requisições HTTP

Até agora vimos que para um gerenciamento mais básico de estado, apesar do Redux nos oferecer mais facilidades, ainda não temos muitas diferenças significativas em relação a utilizar a Context API, porém um ponto muito positivo do Redux é suportar middlewares por padrão, enquanto teríamos que implementar uma high order function (se você não sabe o que é isso, considere visitar nosso artigo sobre programação funcional) em cima do dispatch para isso, a documentação do Redux nos dá uma ideia do processo de como poderíamos implementar isso.

Então já que não temos middlewares por padrão na Context API (até porque ela só serve para propagar o estado, e não para gerenciar ele), teríamos que fazer todas as manipulações assíncronas fora do Reducer (muito provavelmente dentro de um useEffect).

Enquanto que com o Redux, graças ao Redux Toolkit, nós podemos extender a nossa slice com o createAsyncThunk por exemplo:

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "../../app/store/redux";

export const fetchCount = createAsyncThunk("counter/fetchCount", async () => {
  const response = await axios.get("/fakeApi/count");
  return response.count;
});

interface CounterState {
  value: number;
  status: string;
}

const initialState: CounterState = {
  value: 0,
  status: "",
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchCount.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(fetchCount.fulfilled, (state, action) => {
        state.value = action.payload;
        state.status = "idle";
      });
  },
});

export const { increment } = counterSlice.actions;
export const selectCount = (state: RootState) => state.counter.value;
export default counterSlice.reducer;

Além disso, por ser extremamente comum lidarmos com APIs hoje em dia, o pessoal do Redux tem um módulo no Redux Toolkit chamado de RTK Query, que consegue abstrair os endpoints de uma API dentro do Redux, caso se você tenha mais interesse no assunto, pode tentar seguir por essa parte da documentação.

Conclusão

Apesar de muitas pessoas compararem a Context API com o Redux, eles não são a mesma coisa, a Context API é simplesmente um jeito de propagar o estado da aplicação, enquanto o Redux é uma lib focada em gerenciar o estado da aplicação (cuja, uma de suas responsabilidades também é propagar o estado da aplicação, e eles inclusive usam a Context API para isso).

Caso seja um projeto bem simples, não tem problema usar a Context API para implementar um padrão como o Flux por exemplo, e inclusive provavelmente será mais rápido de fazer e seu projeto teria menos dependências, porém conforme o projeto for escalando, o Redux se demonstra uma opção bem superior para gerenciar o estado da aplicação.

Espero que você que leu até aqui tenha aprendido algo novo, e conta para gente aqui em baixo qual das duas abordagens você prefere, até a próxima.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *