TORNAR

Refactoritza TodoMVC amb Redux Starter Kit

7 min de lectura

He treballat amb React durant més de dos anys. Vaig començar en un projecte gran que ja utilitzava Redux. Submergir-me en tant codi existent va ser aclaparador, especialment amb un framework desconegut. Amb el temps, em vaig sentir còmode i experimentat.

Recentment vaig descobrir Redux Starter Kit de l'equip de Redux. Aquest conjunt d'eines simplifica el treball amb Redux. Una eina, createReducer, segueix un patró que utilitzo des de fa temps. Redueix codi repetitiu i accelera el desenvolupament, especialment en projectes nous.

Per aprendre aquestes eines, vaig migrar una base de codi existent amb Redux. Com a exemple, vaig triar l'omnipresent TodoMVC, específicament la versió del repositori de Redux.

Punt de partida

L'app té dos reducers principals: visibilityFilter i todos. Cadascun té les seves pròpies accions, creadors d'accions i selectors.

Filtre de Visibilitat

Vaig començar amb el reducer més simple, després vaig passar al més complex.

Reducer

El reducer de l'exemple de Redux ja és simple i clar.

// 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;
}
};

Redux Starter Kit proporciona createReducer per crear reducers. Com vaig mencionar, ja utilitzo aquest patró i em resulta efectiu.

En lloc de crear un reducer amb una sentència switch case, passes l'estat inicial com a primer paràmetre i un objecte que mapeja tipus d'acció a funcions reducer ((state, action) => { /* codi del reducer */}).

Redueix codi repetitiu i gestiona automàticament el cas default amb return state. El major benefici: millor llegibilitat.

Aquí està 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 les accions. El filtre de visibilitat té una acció, SET_VISIBILITY_FILTER, amb un creador simple:

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

El conjunt d'eines proporciona createAction, que pren només el tipus d'acció com a paràmetre i retorna 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 accepta paràmetres opcionals. Qualsevol argument es converteix en el payload de l'acció:

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'

Ara el filtre usa la clau payload en lloc de filter. Això requereix un petit canvi en el reducer:

// 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

Els selectors són una de les millors eleccions en treballar amb React. Permeten refactoritzar l'estructura de l'estat sense canviar cada component que el consumeix.

El selector del filtre de visibilitat és directe:

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

Utilitzar createSelector afegeix una mica més de codi, però la recompensa arriba aviat. Segueix llegint.

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

Slices

Fins ara, hem reemplaçat funcions simples amb altres més simples utilitzant diversos creadors. Ara ve el veritable poder del conjunt d'eines: createSlice.

createSlice accepta un estat inicial, funcions reducer i un nom de slice opcional. Genera automàticament creadors d'accions, tipus d'accions i selectors.

Ara podem descartar tot el codi anterior.

Crear un slice per al filtre de visibilitat és net i elimina codi repetitiu significatiu.

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

El resultat és un sol objecte amb tot el necessari per treballar amb Redux:

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

Veure tots els canvis fins ara en aquest commit.

Todos

El reducer de todos és més complex, així que explicaré el resultat final en lloc de cada pas. Veure el codi complet aquí.

Primer, definir l'estat inicial:

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

Per millorar la llegibilitat, vaig extreure cada acció del reducer en la seva pròpia funció:

// 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);

Ara combina'ls en un 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 de createSlice simplement retornen valors de l'estat (ex: todos.selectors.getTodos). Aquesta aplicació necessita selectors més complexos.

Per exemple, getVisibleTodos necessita el filtre de visibilitat i els todos. createSelector pren un array de selectors (o rutes de l'estat) com a primer paràmetre i una funció implementant la lògica de selecció com a segon.

// 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),
);

Vaig afegir els nous selectors a l'objecte todos.selectors, mantenint tots els selectors en un sol lloc.

Crear Store

La llibreria també proporciona configureStore i getDefaultMiddleware.

configureStore embolcalla el createStore de Redux. Ofereix la mateixa funcionalitat amb una API més neta--habilitar eines de desenvolupador requereix només un booleà.

getDefaultMiddleware retorna [immutableStateInvariant, thunk, serializableStateInvariant] en desenvolupament i [thunk] en producció.

  • redux-immutable-state-invariant: Detecta mutacions en reducers durant el dispatch i entre dispatches (en selectors o components).
  • serializable-state-invariant-middleware: Comprova estat i accions per a valors no serialitzables com funcions i Promeses.
// 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 redueix codi repetitiu, fent el codi més net i fàcil d'entendre. També accelera el desenvolupament.

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