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)