Refactoritza TodoMVC amb Redux Starter Kit
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.jsimport { 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.jsimport { 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.jsimport * 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.jsimport * 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.jsimport { 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.jsconst 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.jsimport { 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.jsimport { 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.jsconst 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.jsconst 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.jsconst 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.jsconst { 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.jsimport { 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