Coderstand

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.


Brice Coquereau

Bonjour! Je suis Brice Coquereau, parisien, développeur, flexitarien, et amateur de chats. Je vais à des meetups assez souvent. Venez me parler sur Mastodon ou Twitter (mais Mastodon c'est mieux)

TwitterFacebookLinkedin