Redux 101: gerenciadores de estados simplificados
profile image

Roberto Costa

robertheory

Jan 3

21
3
DevTo

Redux 101: gerenciadores de estados simplificados

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

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.

imagem de Componentes distantes

  • 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.

imagem de Prop Drilling

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:

arquitura flux

Neste exemplo, temos uma store de carrinho de compras chamada carrinho e algumas actions que representam operações no carrinho.

exemplo store flux

Logo depois vamos usar nos nossos componentes React:

implementação do flux

  • O componente Carrinho está observando a store, que contém a lista de produtos do carrinho;
  • O componente Produto , bem distante do carrinho, vai acionar a action chamada adicionar ao carrinho;
  • A action vai ser recebida pelo dispatcher que vai interagir com a store;
  • A store recebe a action 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.

Redux Store

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]
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

print de tela

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
            }
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 */);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

E assim atualizamos a nossa Store:
src/store/index.ts

...
// importação atualizada
import storage from './storage';

const persistConfig = {
    key: 'root',
    storage,
};
Enter fullscreen mode Exit fullscreen mode

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