DEV Community

Cover image for React: Organizando seu useReducer
Eduardo Rabelo
Eduardo Rabelo

Posted on • Updated on

React: Organizando seu useReducer

Pequenos padrões criando grandes diferenças!

Com a introdução do React Hooks, a criação de estado local e global ficou um pouco mais simples (dependendo do ponto de vista né?) e toda a criação de estado está propenso a ser puro/imutável, pois a referência do Hook muda a cada renderização.

As duas opções nativas do React são useState e useReducer.

Se você já vem andando por esse mato a algum tempo, pode ter ouvido "use o useState para casos simples e o useReducer para casos complexos" ou "ah mas o useState usa o useReducer por baixo do capô" e para finalizar "o useReducer é o Redux no React, prefiro useState" (🤷‍♂️🤷‍♂️🤷‍♂️).

Opiniões a parte, o useState realmente faz uso do useReducer por baixo do capô, você pode conferir o trecho do código do reconciliador do React no GitHub (o link pode/deve mudar no futuro! 😆).

Eu gosto dos dois, mas hoje, vamos falar do useReducer.

Começando com a documentação

Olhando a documentação de referência do React Hooks, nos temos o seguinte exemplo com useReducer:

let initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  let [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

Com estados pequenos como esse, essa estrutura até que funciona por um bom tempo.

Qual seria o próximo passo então?

Extraindo as ações

Assim como o Redux, a idéia de action creators é bem válida com useReducer. Como eu gosto de ir passo a passo, normalmente começo isolando as ações e criando um objeto com chave (nome da ação) e valor (a função que retorna um novo estado).

Essa função recebe como argumentos o estado atual/anterior e a ação em si. Sempre retornando um novo estado.

Removemos o switch em favor de um if..else, deixando a leitura mais simples. E, nesse caso minha preferência pessoal, ao invés de jogar um erro, eu prefiro logar quais ações não tem um redutor correspondente. Ficando mais simples a iteração entre aplicação no navegador e código.

Chegando ao seguinte código:

let initialState = {count: 0};
let reducerActions = {
    increment: (state, action) => {
      return {count: state.count + 1};
    }
    decrement: (state, action) => {
      return {count: state.count - 1};
    }
};

function reducer(state, action) {
    let fn = reducerActions[action.type];

    if (fn) {
      return fn(state, action);
    }

    console.log('[WARNING] Action without reducer:', action);
    return state;
}

function Counter() {
  let [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

Ficou um pouco melhor. Porém, essas funções no reducerActions precisam retornar um novo estado e, manualmente atualizar seus valores, é propenso a erros! Acredito que você se lembra de cenários como { ...state, chave: { ...state.chave } }, isso já me trouxe muitos pesadelos. 😣

Então, como podemos melhorar essa parte?

Estados imutáveis com operações mutáveis

Uma biblioteca que eu adoro e que também ganhou os prêmios ‌Breakthrough of the year no React Open Source Awards e ‌Most impactful contribution no JavaScript Open Source Award em 2019, é a biblioteca immer.

Com ela, podemos garantir que toda a mudança dentro das nossas funções redutoras irão retornar um novo estado, sem a complicação de ... a cada { ...{ ...{} } } que você criar.

Antes de passar o estado como argumento para nossas funções redutoras, invocamos o immer e retornamos o estado temporário criado para as funções redutoras.

Ficando com o seguinte código:

import immer from 'immer';

let initialState = {count: 0};
let reducerActions = {
    increment: (state, action) => {
      state.count += 1;
    }
    decrement: (state, action) => {
      state.count -= 1;
    }
};

function reducer(state, action) {
    let fn = reducerActions[action.type];

    if (fn) {
      return immer(state, draftState => fn(draftState, action));
    }

    console.log('[WARNING] Action without reducer:', action);
    return state;
}

function Counter() {
  let [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

Como você pode perceber, agora podemos utilizar operações mutáveis dentro do nosso redutor, de modo totalmente seguro. Garantindo que um novo estado imutável/puro seja retornado.

Tudo isso é bem legal nesse exemplo da documentação, mas, como ficaria isso em algo mais dinâmico, como uma chamada de API?

Chamadas de API e o objeto "payload"

Até o momento, não chegamos a usar o segundo argumento da função redutora (state, action), o objeto action foi esquecido. No exemplo a seguir faremos uso dele, porém, com uma chave extra chamada payload.

A chave payload, assim como no Redux, fica encarregada de despachar os dados necessários para a ação atual. Também iremos atualizar nossas funções redutoras para receber apenas o objeto de payload e não o objeto action. Isolando o acesso a qualquer outro tipo de dados desnecessários.

Vamos buscar dados da API do Rick & Morty e montar uma lista com os nomes dos personagens.

Seguindo os exemplos acima, ficamos com o seguinte código:

import immer from "immer";

let initialState = {
  characters: {
    data: null,
    error: null,
    loading: false
  }
};
let reducerActions = {
  fetch_rick_and_morty_pending: (state, payload) => {
    state.characters.loading = true;
    state.characters.error = null;
    state.characters.data = null;
  },
  fetch_rick_and_morty_resolved: (state, payload) => {
    state.characters.loading = false;
    state.characters.error = null;
    state.characters.data = payload.value;
  },
  fetch_rick_and_morty_rejected: (state, payload) => {
    state.characters.loading = false;
    state.characters.error = payload.error;
    state.characters.data = null;
  }
};
let reducer = (state, action) => {
  let fn = reducerActions[action.type];

  if (fn) {
    return immer(state, draftState => fn(draftState, action.payload));
  }

    console.log('[WARNING] Action without reducer:', action);
    return state;
};

function App() {
  let [state, dispatch] = React.useReducer(reducer, initialState);

  React.useEffect(() => {
    let didRun = true;

    async function fetchRickAndMorty() {
      let req = await fetch("https://rickandmortyapi.com/api/character");
      let json = await req.json();
      return json;
    }

    if (state.characters.loading) {
      fetchRickAndMorty()
        .then(data => {
          if (didRun) {
            dispatch({
              type: "fetch_rick_and_morty_resolved",
              payload: { value: data.results }
            });
          }
        })
        .catch(err => {
          if (didRun) {
            dispatch({
              type: "fetch_rick_and_morty_rejected",
              payload: { error: err }
            });
          }
        });
    }

    return () => {
      didRun = false;
    };
  }, [state.characters]);

  let { loading, data, error } = state.characters;

  return (
    <div className="App">
      <button
        type="button"
        onClick={() => dispatch({ type: "fetch_rick_and_morty_pending" })}
      >
        Let's Rick & Morty!
      </button>
      {loading && data === null && <p>Loading characters...</p>}
      {!loading && error !== null && <p>Ooops, something wrong happened!</p>}
      {!loading && data !== null && data.length === 0 && (
        <p>No characters to display.</p>
      )}
      {!loading && data !== null && data.length > 0 && (
        <ul>
          {state.characters.data.map(char => (
            <li key={char.id}>{char.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Como podemos ver, utilizar operações de mutação deixa tudo bem mais simples, especialmente para acessar objetos aninhados no estado.

Gerenciamento de estado é um tópico a parte, que merece sua própria discussão, mas aqui podemos ver alguns padrões de domínios, nomenclatura e ações.

Você pode conferir o exemplo ao vivo em:

https://codesandbox.io/s/live-demo-article-usereducer-fyehh

Finalizando

React Hooks trazem algumas facilidades, mas ainda temos que tomar cuidado com muita coisa, afinal, é JavaScript! Cuidar de valores e referências pode ser uma dor de cabeça se você não está acostumado com nossa amada linguagem.

E aí tem alguma dica para React.useReducer? Ou React.useState? Compartilha aí nos comentários!

Até a próxima! 👋🎉

Top comments (0)