TORNAR

Refactoritza TodoMVC amb Redux Starter Kit

9 min de lectura

He estat treballant amb React més de dos anys ara. Vaig començar en un projecte bastant gran amb molta feina ja feta que estava utilitzant Redux. Va ser una mica aclaparador començar directament amb tantes coses fetes, especialment amb un framework del qual no sabia gaire. Però després de algun temps em vaig sentir més còmode i experimentat.

Recentment vaig descobrir el projecte Redux Starter Kit del mateix equip que treballa en Redux. És un conjunt d'eines simple que proporciona utilitats que poden fer la feina amb Redux realment simple i fàcil. De fet, una de les eines que proporciona, createReducer, és un patró que he estat utilitzant per un temps i m'ajuda molt a reduir el codi repetitiu i accelerar el meu desenvolupament (especialment en nous projectes).

Així que per aprendre més sobre i sentir-me còmode utilitzant-lo, vaig decidir migrar una base de codi ja existent amb Redux, utilitzant aquest conjunt d'eines. Òbviament, com un projecte d'exemple per a un framework frontend, vaig triar l'omnipresent TodoMVC, en concret la versió que Redux proporciona com exemple en el seu repositori.

Punt de partida

Per a qui no sàpiga com es veu aquesta app en Redux, té dos reducers principals visibilityFilter i todos; tots dos amb les seves respectives accions, creadors d'accions i selectors.

Filtre de Visibilitat

Vaig començar amb el reducer més "simple", per començar petit i després moure'm a un estat més complex.

Reducer

El reducer, com venia de l'exemple de Redux, ja és bastant simple i fàcil d'entendre.

// reducers/visibilityFilter.js
import { SET_VISIBILITY_FILTER } from '../constants/ActionTypes';
import { SHOW_ALL } from '../constants/TodoFilters';
export default (state = SHOW_ALL, action) => {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
};

Per crear reducers Redux Starter Kit proporciona una funció createReducer, com vaig mencionar abans és un patró que ja utilitzava i estic bastant feliç amb ell.

La idea és simple, en lloc d'haver de crear una funció reducer amb una sentència switch case dins, aquesta funció espera l'estat inicial com un primer paràmetre i un objecte on les claus són els tipus d'acció i el valor són els reducers ((state, action) => { /* codi del reducer */) per a aquesta acció.

Redueix una mica de codi repetitiu i establirà sempre la sentència default com return state. Però per a mi, el major benefici és la llegibilitat que proporciona.

Així és com es veu el reducer del filtre de visibilitat utilitzant createReducer:

// reducers/visibilityFilter.js
import { createReducer } from 'redux-starter-kit';
import { SET_VISIBILITY_FILTER } from '../constants/ActionTypes';
import { SHOW_ALL } from '../constants/TodoFilters';
export default createReducer(SHOW_ALL, {
[SET_VISIBILITY_FILTER]: (state, action) => action.filter
});

Creadors d'accions

Ara és el moment de les accions. El filtre de visibilitat només té una acció SET_VISIBILITY_FILTER i el creador és molt simple:

// actions/index.js
import * as types from '../constants/ActionTypes';
/* ... Altres accions ...*/
export const setVisibilityFilter = filter => ({
type: types.SET_VISIBILITY_FILTER,
filter
});

Per a les accions, aquest conjunt d'eines pot ser bastant obstinat. Proporciona la funció createAction que només espera el tipus d'acció com a paràmetre. Com a resultat, obtenim un creador d'accions.

// actions/index.js
import * as types from '../constants/ActionTypes';
/* ... Altres accions ...*/
export const setVisibilityFilter = createAction(types.SET_VISIBILITY_FILTER);

Aquest creador d'accions es pot executar amb o sense paràmetres. En el cas que enviem un paràmetre, aquest s'establirà com el payload de l'acció. Aquests són alguns exemples de com funcionarà:

const setVisibilityFilter = createAction('SET_VISIBILITY_FILTER');
let action = setVisibilityFilter();
// { type: 'SET_VISIBILITY_FILTER' }
action = setVisibilityFilter('SHOW_COMPLETED');
// retorna { type: 'SET_VISIBILITY_FILTER', payload: 'SHOW_COMPLETED' }
setVisibilityFilter.toString();
// 'SET_VISIBILITY_FILTER'

Així que ara el filtre s'estableix a la clau payload de l'acció, això implica una refactorització en el reducer ja que estàvem utilitzant la clau filter, però afortunadament és molt simple de canviar.

// reducers/visibilityFilter.js
import { createReducer } from 'redux-starter-kit';
import { SET_VISIBILITY_FILTER } from '../constants/ActionTypes';
import { SHOW_ALL } from '../constants/TodoFilters';
export default createReducer(SHOW_ALL, {
[SET_VISIBILITY_FILTER]: (state, action) => action.payload
});

Selectors

Per a mi utilitzar selectors és una de les millors eleccions que qualsevol pot prendre quan treballa amb React, perquè fa realment simple refactoritzar com es veu l'estat sense haver de canviar tots els components que estan consumint aquesta part de l'estat.

El selector del filtre de visibilitat és un dels més fàcils:

// selectors/index.js
const getVisibilityFilter = state => state.visibilityFilter;
/* ... Altres selectors ...*/

I no canvia massa utilitzant la funció createSelector. Realment, tenim més codi ara que amb la versió anterior, però confia en mi va a ser més simple. Només segueix llegint.

// selectors/index.js
import { createSelector } from 'redux-starter-kit';
const getVisibilityFilter = createSelector(['visibilityFilter']);
/* ... Altres selectors ...*/

Slices

Fins ara l'única cosa que vam fer és canviar algunes funcions simples a funcions més simples utilitzant diferents creadors. Però ara és on vaig descobrir el poder real del conjunt d'eines: createSlice.

createSlice és una funció que accepta un estat inicial, un objecte ple de funcions reducer, i opcionalment un "nom de slice", i automàticament genera creadors d'accions, tipus d'accions i selectors llestos per ser utilitzats.

Ara podem llençar tot el codi que vam fer.

Crear un slice per al filtre de visibilitat és molt net i fàcil d'entendre, i atès que podem llençar tot el codi anterior que vam refactoritzar el resultat final és eliminar molt codi repetitiu.

// ducks/visibilityFilter.js
import { createSlice } from 'redux-starter-kit';
export default createSlice({
slice: 'visibilityFilter',
initialState: SHOW_ALL,
reducers: {
setVisibilityFilter: (state, action) => action.payload
}
});

Ara tenim un sol objecte com a resultat contenint tot el que necessitem per treballar correctament amb Redux. Així és com es pot utilitzar:

const reducer = combineReducers({
visibilityFilter: visibilityFilter.reducer
});
const store = createStore(reducer);
store.dispatch(visibilityFilter.actions.setVisibilityFilter(SHOW_COMPLETED));
// -> { visibilityFilter: 'SHOW_COMPLETED' }
const state = store.getState();
console.log(visibilityFilter.selectors.getVisibilityFilter(state));
// -> SHOW_COMPLETED

Tots els canvis fets fins ara estan en aquest commit.

Todos

El reducer de todos és més complex així que no vaig a mostrar la refactorització pas a pas. En el seu lloc, vaig a explicar com es veu el resultat final, però si estàs interessat ves directament al resultat final.

La primera part és definir l'estat inicial:

// ducks/todos.js
const initialState = [
{
text: 'Use Redux',
completed: false,
id: 0
}
];

Per fer la creació del slice més llegible, vaig extreure les diferents accions del reducer en diferents funcions:

// ducks/todos.js
const addTodo = (state, action) => [
...state,
{
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.payload.text
}
];
const deleteTodo = (state, action) => state.filter(todo => todo.id !== action.payload.id);
const editTodo = (state, action) =>
state.map(todo =>
todo.id === action.payload.id ? { ...todo, text: action.payload.text } : todo
);
const completeTodo = (state, action) =>
state.map(todo =>
todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
);
const completeAllTodos = state => {
const areAllMarked = state.every(todo => todo.completed);
return state.map(todo => ({
...todo,
completed: !areAllMarked
}));
};
const clearCompleted = state => state.filter(todo => todo.completed === false);

I ara podem posar tot junt en un nou slice:

// ducks/todos.js
const todos = createSlice({
slice: 'todos',
initialState,
reducers: {
add: addTodo,
delete: deleteTodo,
edit: editTodo,
complete: completeTodo,
completeAll: completeAllTodos,
clearCompleted: clearCompleted
}
});

Per defecte els selectors retornats per createSlice són molt simples, només retornen el valor de l'estat (ex: todos.selectors.getTodos). Però en aquesta aplicació, necessitem definir selectors més complexos.

Per exemple, getVisibleTodos necessita saber sobre el filtre de visibilitat actual i també els todos. createSelector obté com a primer paràmetre un array amb cadenes (la ruta per seleccionar de l'estat) o altres selectors i com a segon paràmetre la funció que va a implementar la lògica que volem per seleccionar els todos basats en el filtre seleccionat.

// ducks/todos.js
const { getVisibilityFilter } = visibilityFilter.selectors;
todos.selectors.getVisibleTodos = createSelector(
[getVisibilityFilter, todos.selectors.getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case SHOW_ALL:
return todos;
case SHOW_COMPLETED:
return todos.filter(t => t.completed);
case SHOW_ACTIVE:
return todos.filter(t => !t.completed);
default:
throw new Error('Filtre desconegut: ' + visibilityFilter);
}
}
);
todos.selectors.getCompletedTodoCount = createSelector([todos.selectors.getTodos], todos =>
todos.reduce((count, todo) => (todo.completed ? count + 1 : count), 0)
);

Com pots notar en el codi anterior, vaig crear els nous selectors en l' objecte selectors en el slice todos així que ara tenim tots els selectors accessibles en el mateix lloc.

Crear Store

Les últimes dues funcions proporcionades per la llibreria són configureStore i getDefaultMiddleware.

configureStore és una abstracció sobre la funció estàndard de Redux createStore. No proporciona més funcionalitats que createStore però fa les coses més fàcils de llegir, com habilitar eines de desenvolupador que és només un booleà.

getDefaultMiddleware retorna una llista de middlewares [immutableStateInvariant, thunk, serializableStateInvariant] en desenvolupament i [thunk] en producció.

  • redux-immutable-state-invariant: Pot detectar mutacions en reducers durant un dispatch, i també mutacions que ocorren entre dispatches (ex: en selectors o components).
  • serializable-state-invariant-middleware: Comprova profundament el teu arbre d'estat i les teves accions per a valors no serialitzables com funcions, Promeses, etc.
// store.js
import { configureStore, getDefaultMiddleware } from 'redux-starter-kit';
import { combineReducers } from 'redux';
import { visibilityFilter, todos } from './ducks';
const preloadedState = {
todos: [
{
text: 'Use Redux',
completed: false,
id: 0
}
]
};
const reducer = combineReducers({
todos: todos.reducer,
visibilityFilter: visibilityFilter.reducer
});
const middleware = [...getDefaultMiddleware()];
export const store = configureStore({
reducer,
middleware,
devTools: process.env.NODE_ENV !== 'production',
preloadedState
});

Pensaments finals

Redux Starter Kit sembla interessant, redueix el codi repetitiu fent el codi més net i fàcil d'entendre. Però també fa realment ràpid desenvolupar nou codi.

Codi Font: https://github.com/magarcia/todomvc-redux-starter-kit