React: Organizando seu useReducer

Pequenos padrões criando grandes diferenças!

Image for post
Image for post

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

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

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

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”

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:

Finalizando

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

Até a próxima! 👋🎉

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store