Redux: Faire un reducer sans switch
tags: Flux , React , Angular , Vue , Javascript
06/10/2017 (c'était un vendredi)
Dans tous les exemples de Redux (et dans ceux de Ngrx pour Angular), les reducers utilisent un switch pour déterminer l’action à faire.
// Les actions sont déclarées dans un autre fichier,
// mais pour simplifier on les met ici
const SET_VISIBILITY_FILTER = "setvisibilityfilter";
const ADD_TODO = "addtodo";
export function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
Le problème avec cette écriture, c’est qu’on obtient très vite une fonction énorme, ce qui la rend difficile à lire (et Sonar nous insulte à cause de la complexité).
En regardant la doc de Vue, la notation avec des constantes m’a beaucoup plu. On va faire pareil en React, et même aller encore plus loin.
Des petites fonctions
Pour commencer, on va séparer chaque traitement dans une fonction. Comme tous les reducers sont synchrones, on va pouvoir passer uniquement le state et l’action. Pour une meilleure lisibilité, on va utiliser la décomposition au lieu de Object.assign.
const setVisibilityFilter = (state, action) => {
return {
...state,
visibilityFilter: action.filter
};
}
const addTodo = (state, action) => {
return {
...state,
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
};
}
export function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return setVisibilityFilter(state, action);
case ADD_TODO:
return addTodo(state, action);
default:
return state
}
}
Enlever le switch
Un switch est une source facile d’erreurs. Un break
ou un return
oublié,
et c’est le drame. Pour le remplacer, on va créer un objet qui associera le type de l’action à son reducer.
Ici, on utilise [les “Noms calculés pour les propriétés” (computed property name)]
(https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Op%C3%A9rateurs/Initialisateurobjet#NouvellesnotationsECMAScript2015_(ES6))
// les fonctions setVisibilityFilter et addTodo ne bougent pas
const actions = {
[SET_VISIBILITY_FILTER]: setVisibilityFilter
[ADD_TODO]: addTodo
};
/*
Cela crée l'object suivant:
actions = {
"setvisibilityfilter": setVisibilityFilter,
"addtodo": addTodo
}
*/
export function todoApp(state = initialState, action) {
// On récupère la fonction associée à l'action
const reducer = actions[action.type];
if(reducer !== undefined) {
// On connait l'action demandée, on renvoie le résultat du réducer
// (qui doit toujours renvoyer le state complet)
return reducer(state, action);
} else {
// Si on ne connait pas l'action, on renvoie le state précédent
return state;
}
}
Abstraction
En regardant notre fonction todoApp, on se rend compte qu’elle n’a plus rien de spécifique. On peut donc extraire une fonction générique, qu’on utilisera dans tous nos reducers.
Dans Redux, il faut toujours exposer une fonction qui prend (state, action) en paramètre, on ne peut donc pas créer une fonction générique qui prendrait (actions, initialState, state, action). Pour palier à ce problème, on va donc écrire une fonction qui prend (actions, initialState), et qui renvoie une seconde fonction qui va prendre (state, action), comme attendu par Redux.
// Fichier reducerCreator.js
export default function reducerCreator(reducers, initialState) {
return (state = initialState, action) => {
const reducer = reducers[action.type];
if (reducer !== undefined) {
return reducer(state, action);
}
return state;
};
}
//Fichier todoApp.js
import reducerCreator from 'reducerCreator';
// Ici, il ne nous reste qu'à paramétrer reducerCreator,
// en configurant quelle action est liée à quel reducer,
// et un state initial éventuel
const actions = {
[SET_VISIBILITY_FILTER]: setVisibilityFilter
[ADD_TODO]: addTodo
};
const initialState = {
todos: []
}
export reducerCreator(actions, initialState);
Conclusion
Cette fonction reducerCreator
, totalement générique,
peut maintenant être utilisée sur tous les projets.
De plus, comme nous avons séparé chaque reducer dans une fonction, on peut les tester un à un beaucoup plus facilement.
Et le bonus final, c’est que Sonar ne nous tapera pas dessus à cause de la complexité des fonctions, car on a maintenant plein de petites fonctions.