Reduxed — Redux com menos boilerplate

Carlos Roberto Gomes Junior
5 min readMar 20, 2019

Você também pode ler este artigo em inglês aqui.

A library Redux com certeza se consolidou como uma das melhores bibliotecas para gerenciamento de estado em aplicações React. Mas uma coisa é certa, existe muito boilerplate de código a ser feito para que tudo funcione como esperado.

É sempre a mesma história. Para cada ponto da aplicação temos que criar:

  • Constants para o tipo de ação a ser disparada.
  • Action Creators para criar ações.
  • Reducers com vários switch cases.
  • Prefixos para evitar conflitos de tipo (ex: {type: “formX/SUBMIT”}).

Se você está trabalhando em uma aplicação grande provavelmente já se viu fazendo isso muitas vezes.

Dependendo da forma como você organiza o seu código você pode terminar com uma infinidade de arquivos, como por exemplo:

state
├── auth
│ ├── actions.js
│ ├── reducer.js
│ └── types.js
├── dashboard
│ ├── actions.js
│ ├── reducer.js
│ └── types.js
├── registration
│ ├── actions.js
│ ├── reducer.js
│ └── types.js
└── settings
├── actions.js
├── reducer.js
└── types.js

Faz algum tempo Erik Rasmussen propôs um padrão para simplificar isso, ele chamou de Ducks: Redux Reducer Bundles, cuja idéia é ao invés de criar essa separação entre reducers, action creators e type constants, criamos apenas um módulo que contenha tudo o que está relacionado com aquela parte do estado da aplicação.

Aplicando o padrão Ducks o nosso exemplo acima ficaria assim:

state
├── auth.js
├── dashboard.js
├── registration.js
└── settings.js

Como tudo está relacionado faz bastante sentido ter apenas um módulo que exporte tudo. Isso pode reduzir a complexidade de manutenção da sua aplicação conforme ela cresce.

Porém mesmo aplicando o padrão Ducks, ainda não eliminamos a necessidade de criar reducers, action creators e type constants.

Para simplificar esse excesso de código criei a library Reduxed — Reduced Redux Boilerplate, que nos permite definir o estado de uma aplicação de forma bem mais simples.

O que é Reduxed?

Reduxed não é uma library para substituir o Redux, mas sim para ser utilizada em conjunto com ele. A idéia principal é que você apenas defina seus reducers, sendo um para cada action (sem vários switch case) e partir deles o Reduxed se encarrega de gerar as constantes de tipo, action creators e um reducer final.

Como funciona?

A melhor forma de entender é comparando um caso onde não utilizamos o Reduxed e outro utilizando ele.

Sem Reduxed

// prefix
const prefix = 'app/counter';
// types
export const INCREMENT = `${prefix}/INCREMENT`;
export const DECREMENT = `${prefix}/DECREMENT`;

// action creators
export const increment = (value = 1) =>
({ type: INCREMENT, payload: value });
export const decrement = (value = 1) =>
({ type: DECREMENT, payload: value });

const initialState = 0;

// reducer
export const reducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return state + action.payload;

case DECREMENT:
return state - action.payload;

default:
return state;
}
}

Com Reduxed

Importamos as funções necessárias:

import { 
create,
handler,
getActions,
getReducer,
getTypes
} from "reduxed";

Definimos o estado inicial e configurações:

const initialState = 0;
const options = { typePrefix: "app/counter" };

Definimos nossos reducers para cada action usando as funções create e handler:

const counter = create(
handler("increment", (state, payload = 1) => state + payload),
handler("decrement", (state, payload = 1) => state - payload)
)(initialState, options); // options are optional

A função handler sempre espera dois argumentos: um nome e um reducer que recebe o state atual e o payload enviado por uma action (como veremos mais abaixo).

Nada nos impede de exportarmos esses reducers para escrever testes por exemplo:

export const increment = (state, payload = 1) => state + payload;
export const decrement = (state, payload = 1) => state - payload;
const counter = create(
handler("increment", increment),
handler("decrement", decrement),
)(initialState, options); // options are optional

Finalmente utilizamos as funções getReducer , getActions e getTypes para obter o reducer, actions e as type constants:

export const reducer = getReducer(counter);
export const actions = getActions(counter);
export const types = getTypes(counter);

getReducer vai retornar um reducer como esperado pelo Redux:

reducer(1, { type: 'app/counter/INCREMENT' }) // 2

getActions vai retornar um objeto como o seguinte:

{
increment: payload => ({
type: 'app/counter/INCREMENT',
payload
}),
decrement: payload => ({
type: 'app/counter/DECREMENT',
payload
}),
}

Note que cada action espera um parâmetro payload.

getTypes vai retornar um objeto como o seguinte:

{
increment: 'app/counter/INCREMENT',
decrement: 'app/counter/DECREMENT',
}

Logo, juntando tudo:

const initialState = 0;
const options = { typePrefix: "app/counter" };
const counter = create(
handler("increment", (state, payload = 1) => state + payload),
handler("decrement", (state, payload = 1) => state - payload)
)(initialState, options); // options are optional
export const reducer = getReducer(counter);
export const actions = getActions(counter);
export const types = getTypes(counter);

Depois nos outros módulos da aplicação importamos apenas o necessário:

// state/rootReducer.js
import { combineReducers } from 'redux';
import { reducer as counterReducer } from './counter';
const rootReducer = combineReducers({
counter: counterReducer,
});
// SomeComponent.js
import { actions } from 'state/counter';
// ...export default connect(
mapStateToProps,
actions,
)(SomeComponent);
// someSaga.js
import { types } from './counter';
import { take } from 'redux-saga/effects';
function* someSagaWorker({ payload }) {
yield take(types.increment);
// ...
}

Reusing reducer logic

Algumas vezes precisamos reusar a lógica de nossos reducers, e geralmente fazer isso não é algo tão simples. Na documentação do Redux encontramos algumas maneiras de como fazer isso. O Reduxed também prove uma forma de fazer isso.

Suponha que você precise de vários componentes Counter no mesmo lugar, cada um com seu estado independente no Redux. Você pode usar a função withScope do Reduxed:

import { combineReducers } from 'redux';
import { withScope } from 'reduxed';
import { reducer } from './counter';
const rootReducer = combineReducers({
counterA: withScope('A', reducer),
counterB: withScope('B', reducer),
counterC: withScope('C', reducer),
});

No mapDispatchToProps do seu componente conectado ao Redux você pode fazer algo como no exemplo abaixo:

import { withScope } from 'reduxed';
import { bindActionCreators } from 'redux';
import { actions } from './counter';
const mapDispatchToProps = (dispatch, ownProps) => {
const scopedActions = withScope(ownProps.scope, actions);
return bindActionCreators(scopedActions, dispatch);
}

E por fim, onde você for utilizar os vários componentes Counters:

const Counters = () => (
<div>
<Counter scope="A" />
<Counter scope="B" />
<Counter scope="C" />
</div>
);

Finalizando

Reduxed ainda é algo novo, embora eu já tenha usado em alguns projetos, e ele realmente ajuda a deixar as coisas mais simples e escrevemos menos código. Aqui você pode ver um exemplo real de uma pequena aplicação onde eu utilizei ele.

Se você gostou, tiver alguma crítica, dúvida, sugestão ou mesmo quiser contribuir, se sinta livre para comentar aqui, abrir uma issue, um pull request ou mesmo testar em seu projeto.

Reduxed — Reduced Redux Boilerplate
https://github.com/carlosrberto/reduxed

Obrigado e até a próxima!

--

--

Carlos Roberto Gomes Junior

JavaScript Developer | Functional Programming Lover | Blockchain Enthusiastic