Redux 101: gerenciadores de estados simplificados
Roberto Costa
Posted on January 3, 2024
Estados complexos são um desafio, então você decide usar Redux, mas encontra outro desafio: entender o Redux. Mas hoje vamos ver tudo que você precisa saber para começar bem com essa ferramenta!
Table of contents
- O que é o Redux
- Quando devo utilizar
- Quando não devo utilizar
- Como funciona o Redux
- Mão na massa!
- Persistindo dados
- Redux com NextJs
- Referências
O que é o Redux
Basicamente, Redux é uma solução para gerenciar estados, ele brilha de verdade quando temos que lidar com casos mais complexos como estados distribuídos e Prop Drilling.
De acordo com a sua documentação:
O Redux é um contêiner de estado previsível para aplicativos JavaScript.
Ele ajuda a escrever aplicativos que se comportam de forma consistente, são executados em diferentes ambientes (cliente, servidor e nativo) e são fáceis de testar.
Além disso, ele fornece uma ótima experiência de desenvolvedor, como a edição de código ao vivo combinada com um depurador que viaja no tempo.
E realmente, essa biblioteca é bem versátil em trabalhar com outras bibliotecas e frameworks de visualização, tornando uma ótima escolha para trabalhar com gerenciamento de estados.
Contudo, é muito fácil fazer um uso equivocado desta ferramenta, devido a sua popularidade e praticidade, muitos projetos adicionam o Redux automaticamente sem refletir se realmente precisam disso.
Quando devo utilizar
Como muitas coisas no mundo da tecnologia, o melhor uso do Redux vem quando você percebe sua necessidade na aplicação, e alguns sinais disso são:
Complexidade do Estado:
Se sua aplicação tem um estado complexo, com múltiplos componentes que precisam acessar e modificar esse estado, o Redux pode fornecer uma maneira mais organizada de gerenciar o estado global.Compartilhamento de Estado:
Quando há a necessidade de compartilhar estado entre componentes que não têm uma relação pai-filho direta, o Redux oferece uma solução centralizada para acessar e modificar o estado.
- Prop Drilling: Quando existe uma relação pai-filho, mas para passar o estado para o filho desejado você precisa passar por vários componentes filhos até chegar nele.
Quando não devo utilizar
Aplicação pequena ou simples:
Se sua aplicação é pequena e possui requisitos de gerenciamento de estado simples, a introdução do Redux pode ser excessiva.Curva de Aprendizado da Equipe:
Se sua equipe não tem familiaridade com o Redux e a curva de aprendizado é um fator crítico, pode ser mais sensato evitar a introdução desta ferramenta, especialmente se os benefícios esperados não justificarem o tempo de aprendizado.-
Ferramentas Alternativas:
Existem várias alternativas no que se diz respeito a gerenciar estados no ReactJS, dentre elas podemos citar:
Mas existe algo melhor ainda: React Context e useReducer hook
Nada melhor do que usar as ferramentas nativas do próprio ReactJS para resolver as questões que temos, assim podemos depender de menos bibliotecas externas e tornando nosso projeto mais simples.
Apesar desta abordagem ser algo que eu destaco como ideal, ou pelo menos mais interassante, é assunto para outro artigo, já que hoje vamos falar de Redux.
Como funciona o Redux
Arquitetura Flux
A arquitetura do Flux foi proposta pelo Facebook para construção de SPAs que divide a aplicação nas seguintes partes:
- View: um componente React, ou Angular, etc;
- Action: um evento que descreve a mudança que está ocorrendo e carrega os dados necessários;
-
Dispatcher: responsável por levar os eventos das actions até a
store
responsável; - Store: recebe os eventos e lida com as mudanças de estado.
Importante ressaltar que:
- O estado é imutável, logo não pode ser modificado diretamente;
- A única forma de atualizar o estado é através das actions e dispatchers
Criei um exemplo visual para entendermos como isso funciona:
Neste exemplo, temos uma store
de carrinho de compras chamada carrinho
e algumas actions
que representam operações no carrinho.
Logo depois vamos usar nos nossos componentes React:
- O componente
Carrinho
está observando astore
, que contém a lista de produtos do carrinho; - O componente
Produto
, bem distante do carrinho, vai acionar aaction
chamadaadicionar ao carrinho
; - A
action
vai ser recebida pelodispatcher
que vai interagir com astore
; - A
store
recebe aaction
e atualiza seu estado com o novo produto; - O carrinho recebe o novo estado e se atualiza.
Se você entendeu este fluxo, praticamente entendeu toda a lógica maior do Redux, pois ele segue fielmente esta arquitetura!
Mergulhando no Redux
Agora que já falamos sobre a arquitetura por trás de tudo, vamos ver como que o Redux funciona de fato. Vamos ver alguns termos e conceitos a se familiarizar na prática desta ferramenta.
Slices
É importante notar que a store é um estado "global" que contém outros estados, frequentemente chamados de "fatias" ou slices
, então o nosso estado de carrinho é um slice
.
Os Reducers
Temos um novo jogador no campo, os reducers são funções super simples que são utilizadas para atualizar o estado da aplicação.
Eles tem responsabilidades e escopos muito bem definidos, assim como as actions:
function addToCart(state, action) {
return {
cart: [...state.cart, action.product]
}
}
Mão na massa!
Nada melhor do que um exemplo de código para entendermos como ligar nossa aplicação ReactJS com o Redux.
Para este propósito fiz o seguinte projeto didático: React State Management 101
Essa demo é uma loja fictícia onde temos duas funcionalidades básicas para atender: gerenciar favoritos e carrinho de compras.
Como nosso intuito aqui é a implementação do Redux, não vamos focar em detalhes como estilização..
Funcionalidades
Antes mesmo de colocar a mão no código, precisamos entender as funcionalidades pretendidas nesta aplicação:
- Favoritar Adiciona o item à lista de favoritos. Caso este item já seja um favorito, ele será removido da lista.
- Adicionar ao carrinho Adiciona este item ao carrinho. Caso este item já esteja no carrinho, sua quantidade deve ser incrementada.
- Remover do carrinho Decrementa a quantidade do item no carrinho. Caso o item tenha apenas 1 de quantidade, é removido do carrinho.
- Deletar do carrinho Remove completamente o item do carrinho.
Implementação
Entraremos agora em detalhes de implementação do projeto, contudo não vou demonstrar todas as funcionalidades, apenas a de favoritar para simplificar este exemplo.
1 Preparação
Começando do zero em um projeto ReactJS recém criado, execute:
yarn add @reduxjs/toolkit
ou
npm install @reduxjs/toolkit
Tendo em vista a atual implementação recomendada na documentação do Redux, temos uma maneira mais interessante de seguir usando ReactJS: vamos utilizar o React-Redux, uma forma de interagir melhor com os componentes React usando todo o poder da biblioteca original.
yarn add react-redux
ou
npm install react-redux
Agora estamos prontos para o primeiro passo: criar a nossa Store!
2 Criando a Store
A Store é o centro de tudo, a partir dela teremos nossos Slices e usaremos em toda a aplicação, é o coração do gerenciamento de estado.
A partir da documentação oficial do React-Redux, temos o seguinte exemplo de código:
src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
// ...
const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer,
},
})
// Infer the RootState and AppDispatch types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
Como ainda não temos nenhum Slice, vamos criar assim por enquanto:
src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Com a Store criada, agora vamos para o próximo passo: configurar o Provider, assim toda aplicação poderá utilizar o Redux.
3 Configurando o Provider
Este passo é o mais simples, nunca realmente vai mudar, apenas não esqueça pois é crucial.
Seguindo a documentação oficial, envolva o seu App com o Provider do React-Redux e passe para ele a sua Store recém criada.
src/index.ts
...
import { Provider } from 'react-redux'
import store from './store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
Agora chegou a hora de criarmos nossos Slices, através deles teremos nossos estados e reducers que vão fazer a mágica acontecer.
4 Slices
Dentro da pasta store
vamos criar nosso primeiro e mais simples Slice: favoritesSlice.ts
Antes de mais nada, gostaria de demonstrar a anatomia geral de um Slice, o que deve se repetir para todos os próximos:
src/store/favoritesSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import { RootState } from '.';
import { Movie } from '../interfaces';
// declarei aqui o estado inicial para que assim pudesse inferir um tipo
// desta forma o slice irá aceitar apenas filmes em seu estado
const initialState: Movie[] = [];
export const favorites = createSlice({
// nome identificador do slice
name: 'favorites',
// estado inicial
initialState,
reducers: {
// funções reducers
},
});
export const {
// reducers
} = favorites.actions;
// seletores (é uma forma de buscar dados dentro dos componentes react usando estas funções que podemos criar)
export const getFavorites = () => (state: RootState) => state.favorites;
// RootState vai dar erro pois este slice ainda não está registrado na store, vamos ver a seguir...
export default favorites.reducer;
Como o Slice é apenas uma fatia do estado global, precisamos que ele se integre com a nossa Store criada anteriormente, e para isso faremos o seguinte:
src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import favorites from './favoritesSlice';
const store = configureStore({
reducer: {
favorites // registrar o novo slice na Store
},
})
// agora o RootState conhece o seu novo slice e deve corrigir o erro anterior
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Agora que vimos como é a estrutura geral de uma Store, vamos à criação dos seus Reducers!
5 Reducers
Vamos criar o reducer de favoritar, e como descrito anteriormente, ele deve funcionar como um botão de liga/desliga, então chamaremos de toggleFavorite
:
src/store/favoritesSlice.ts
export const favorites = createSlice({
name: 'favorites',
initialState,
reducers: {
// state: o estado de favoritos
// action: a ação que está sendo realizada no slice, que por sua vez contém dados na em action.payload
toggleFavorite: (state, action: { payload: Movie }) => {
// a payload é onde podemos passar dados para o nosso reducer
// neste caso fiz obrigatório o envio de um dado do tipo Movie na payload
const index = state.findIndex((movie) => movie.id === action.payload.id);
if (index === -1) {
state.push(action.payload);
} else {
state.splice(index, 1);
}
},
},
});
Desta forma nosso primeiro reducer ganha vida, mas ainda não podemos usar ainda, temos que exportá-lo nas nossas actions:
src/store/favoritesSlice.ts
export const { toggleFavorite } = favorites.actions;
E agora sim, nosso slice de favoritos está pronto para ser utilizado!
Ligado os dados aos componentes!
A página de favoritos é super simples, apenas iremos listar todos eles:
src/pages/favorites/index.tsx
import { useSelector } from 'react-redux';
import Layout from '../../components/Layout';
import MovieCard from '../../components/MovieCard';
import { getFavorites } from '../../store/favoritesSlice';
const Favorites = () => {
// a função useSelector do react-redux é responsável por interpretar a função getFavorites e retornar os dados
const favorites = useSelector(getFavorites());
return (
<Layout>
<h1>Favorites</h1>
<div>
{favorites.length === 0 ? (
<p>You don't have any favorite movies</p>
) : (
favorites.map((movie) => <MovieCard key={movie.id} movie={movie} />)
)}
</div>
</Layout>
);
};
export default Favorites;
Logo vamos seguir para onde acontece toda ação: no componente MovieCard.tsx
src/components/MovieCard.tsx
import { FiHeart, FiShoppingCart } from 'react-icons/fi';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { Movie } from '../interfaces';
import { getFavorites, toggleFavorite } from '../store/favoritesSlice';
type MovieCardProps = {
movie: Movie;
};
const MovieCard = ({ movie }: MovieCardProps) => {
const navigate = useNavigate();
// aqui utilizamos o useDispatch para que o Dispatcher do Redux possa executar os reducers que passarmos
const dispatch = useDispatch();
// novamente buscamos todos os favoritos com o seletor
const favorites = useSelector(getFavorites());
const isFavorite = !!favorites.find((fav) => fav.id === movie.id);
const handleAddToFavorites = () => {
// aqui despachamos o reducer toggleFavorite passando os dados do filme em questão
dispatch(toggleFavorite(movie));
};
const price = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(movie.price);
return (
<div>
<h2>
{movie.title}
</h2>
<img src={movie.cover} alt={movie.title}/>
<div>
<span className='text-lg font-bold mr-auto'>Price: {price}</span>
<button>
<FiShoppingCart size={24} />
</button>
<button
onClick={handleAddToFavorites}
className={isFavorite && 'bg-red-500'}>
<FiHeart
size={24}
className={isFavorite && 'text-red-500'}
/>
</button>
</div>
<button
onClick={() => navigate(`/movie/${movie.id}`)}>
More
</button>
</div>
);
};
export default MovieCard;
Agora o componente MovieCard
é capaz e favoritar os filmes e nossa página de favoritos irá exibi-los, aparentemente está tudo funcionando, mas existe um pequeno problema: Estados React são reiniciados quando a tela atualiza.
Persistindo dados
Uma vez que os dados da nossa Store naturalmente serão jogados fora ao recarregar a página, precisamos que estes dados sejam armazenados localmente no navegador do usuário.
Desta forma mesmo que feche o navegador ou desligue o computador, quando voltar para a aplicação seus favoritos estarão no mesmo lugar onde deixou.
Em alguns casos esta questão é trivial, nem sempre existe a necessidade de armazenar estes dados, mas nesta aqui é super importante que eu persista estes favoritos.
Mas a solução é simples: dentro do reducer, depois que atualizar o estado, utilize o localStore ou sessionStorage para armazenar estes dados no navegador do usuário, e no initialState é só buscar os dados no navegador!
Em teoria sim, isso funciona, mas vai dar um trabalho enorme para implementar isso na mão, fora outras questões que existem.
Mas não tem problema, temos o Redux-Persist, uma solução completa e simplista para persistir os dados do seu Redux.
Vamos à implementação:
Primeiro instale o pacote:
yarn add redux-persist
ou
npm i redux-persist
O passo essencial agora é adaptar nossa Store para usar o Redux Persist:
src/store/index.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import cart from './cartSlice';
import favorites from './favoritesSlice';
// configuração básica do redux-persist
const persistConfig = {
key: 'root',
// podemos utilizar outras formas de armazenamento, mas a padrão utiliza a localStorage do navegador
storage,
};
// vamos mudar a forma como recebemos os reducers
// agora combinamos todos os reducers em um único
const rootReducer = combineReducers({
favorites
});
// o persistedReducer vai comportar dos nossos reducers e com as configurações apropriadas que definimos
const persistedReducer = persistReducer(persistConfig, rootReducer);
// a store é finalmente criada com nosso reducer especial
export const store = configureStore({
reducer: persistedReducer,
});
// responsável por persistir os dados da store
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
E por último, vamos configurar o provider do Redux-Persist:
src/index.ts
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { persistor, store } from '../store';
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
// o mesmo provider que utilizamos antes
<Provider store={store}>
// agora temos que passar o PersistGate em volta da aplicação
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
)
Simples assim, agora os dados irão naturalmente persistir no localStorage.
Ainda sim vale a pena que você se familiarize com temas comuns do Redux-Persist como:
- Blacklist: uma lista dos slices que não devem ser permitidos persistir
- Whitelist: uma lista dos únicos slices que devem persistir
- stateReconciler: Os reconciliadores de estado definem como o estado de entrada é mesclado com o estado inicial
Todos estes temas você encontra na
documentação oficial do Redux-Persist.
Seletores personalizados
Este ponto é bem simples mas ainda sim gostaria de trazer atenção para ele pois seu bom uso é extremamente benéfico no React.
Esta pequena função a seguir faz parte do exemplo dado acima na criação do slice.
app/store/favoritesSlice.ts
export const getFavorites = () => (state: RootState) => state.favorites;
Neste exemplo apenas retornamos todos os favoritos disponíveis no slice, mas podemos realizar diversas modificação de acordo com a necessidade, como por exemplo:
app/store/favoritesSlice.ts
// passando algum parâmetro
export const getSortedFavorites = (sorting:'asc'|'desc') => (state: RootState) => state.favorites.sort(.../* função para ordenar */);
// Editando o retorno
export const getFormattedFavorites = () => (state: RootState) => state.favorites.map(.../* função para formatar os dados */);
Redux com NextJS
Existe um pequeno problema ao utilizar Redux com NextJS:
NextJS é executado no lado do cliente e no lado do servidor, e no lado do servidor NÃO EXISTE WEB STORAGE!
Contudo ainda tem solução:
- Quando a aplicação for executada no lado do cliente utilizaremos o storage padrão;
- Quando a aplicação for executada no lado do servidor utilizaremos o storage fake.
Nossas funções do Redux estão sendo utilizadas apenas no lado do cliente, mas mesmo assim, encontraremos erros em tempo de execução devido a natureza de Server Side Rendering do NextJS.
Logo, vamos criar nosso novo storage:
src/store/storage.ts
import createWebStorage from 'redux-persist/lib/storage/createWebStorage';
const createNoopStorage = () => {
return {
getItem(_key: any) {
return Promise.resolve(null);
},
setItem(_key: any, value: any) {
return Promise.resolve(value);
},
removeItem(_key: any) {
return Promise.resolve();
},
};
};
const storage =
typeof window !== 'undefined'
? createWebStorage('local')
: createNoopStorage();
export default storage;
E assim atualizamos a nossa Store:
src/store/index.ts
...
// importação atualizada
import storage from './storage';
const persistConfig = {
key: 'root',
storage,
};
Gerenciamento avançado de estados é um tema crucial quando se fala de desenvolvimento web, compreender e utilizar a ferramenta Redux e outros semelhantes me ajudou bastante para desenvolver softwares mais sofisticados e complexos.
Espero que tenham gostado desta super introdução ao tema e à ferramenta.
Se quiser deixar alguma nota, complementar em algo ou se houver alguma ideia interessante, não deixe de compartilhar comigo.
Referências
Dias, D. L. (2022, 20 de fevereiro). Flux: A arquitetura JavaScript que funciona. Medium. https://dayvsonlima.medium.com/flux-a-arquitetura-javascript-que-funciona-1197857464b8
FreeCodeCamp.org. (2022, 2 de agosto). Evite prop drilling em React. FreeCodeCamp.org. https://www.freecodecamp.org/news/avoid-prop-drilling-in-react/
Redux.js.org. (s.d.). Redux. Redux.js.org. https://redux.js.org/
React-redux.js.org. (s.d.). React-redux. React-redux.js.org. https://react-redux.js.org/
FreeCodeCamp.org. (2022, 12 de março). O que é Redux store, ações e redutores explicados. FreeCodeCamp.org. https://www.freecodecamp.org/news/what-is-redux-store-actions-reducers-explained/
GitHub. (s.d.). redux-persist. GitHub. https://github.com/rt2zz/redux-persist
Posted on January 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.