Faire Ă©voluer une application Redux avec des canards

En prévision du début du cours, "développeur React.js" a préparé une traduction de matériel utile.





Comment Ă©volue le front-end de votre application? Comment rendre votre code compatible six mois plus tard?

En 2015, Redux a pris d'assaut le monde du développement frontal et s'est imposé comme une norme au-delà de React.

La société pour laquelle je travaille a récemment terminé la refactorisation d'une grande base de code sur React, où nous avons implémenté le redux au lieu du reflux .

Nous avons dû franchir cette étape car aller de l'avant n'était pas possible sans une application bien structurée et un ensemble de règles claires.

La base de code existe depuis plus de deux ans et le reflux y est depuis le tout début. Nous avons dû changer le code, fortement lié aux composants React, auxquels personne n'avait touché depuis plus d'un an.
Sur la base de l'expérience du travail effectué, j'ai créé ce référentiel , qui aidera à expliquer notre approche de l'organisation du code sur redux.
Lorsque vous en apprendrez plus sur redux, les actions et les réducteurs , vous commencez avec des exemples simples. De nombreux tutoriels disponibles aujourd'hui ne les dépassent pas. Cependant, si vous créez quelque chose de plus compliqué sur Redux qu'une liste de tâches, vous aurez besoin d'un moyen raisonnable de mettre à l'échelle votre base de code au fil du temps.

Quelqu'un a dit un jour qu'en informatique, il n'y a pas de tâche plus difficile que de donner des noms de choses différentes. Je ne pouvais pas être en désaccord. Dans ce cas, la structuration des dossiers et l'organisation des fichiers seront en deuxième position.

Voyons comment nous abordions l'organisation du code.

Fonction vs fonctionnalité


Il existe deux approches généralement acceptées pour organiser les applications: la fonction d'abord et la fonction d'abord.
Dans la capture d'écran à gauche, la structure des dossiers est organisée selon le principe de la fonction en premier et à droite, de la fonctionnalité en premier.



La fonction d'abord signifie que vos répertoires de niveau supérieur sont nommés en fonction des fichiers qu'ils contiennent. Vous avez donc: conteneurs, composants, actions, réducteurs , etc.

Cela ne change pas du tout. Au fur et à mesure que votre application se développe et que de nouvelles fonctionnalités apparaissent, vous ajouterez des fichiers dans les mêmes dossiers. Par conséquent, vous devrez faire défiler le contenu de l'un des dossiers pendant une longue période pour trouver le fichier souhaité.

Un autre problème est la combinaison de dossiers. L'un de vos fils d'application nécessitera probablement l'accès aux fichiers de tous les dossiers.

L'un des avantages de cette approche est qu'elle peut isoler, dans notre cas, React de Redux. Par conséquent, si vous souhaitez modifier la bibliothèque de gestion d'état, vous saurez de quels dossiers vous aurez besoin. Si vous devez modifier la bibliothèque de vues, vous pouvez laisser les dossiers avec redux intacts.

La fonctionnalité en premier signifie que les répertoires de niveau supérieur seront nommés conformément à la fonctionnalité principale de l'application: produit, panier, session.

Cette approche évolue beaucoup mieux, car chaque nouvelle fonctionnalité se trouve dans un nouveau dossier. Cependant, vous n'avez pas de séparation entre les composants Redux et React. Changer l'un d'eux à long terme n'est pas une tâche facile.

De plus, vous aurez des fichiers qui n'appartiendront à aucune fonction. En conséquence, tout se résume au dossier commun ou partagé, car vous souhaitez également utiliser votre code dans différentes fonctionnalités de votre application.

Combiner le meilleur de deux mondes


Bien que ce ne soit pas le sujet de l'article, je tiens à dire que les fichiers de gestion d'état des fichiers d'interface utilisateur doivent être stockés séparément.

Pensez à votre application à long terme. Imaginez ce qui arrive à votre code si vous passez de React à autre chose. Ou pensez à la façon dont votre base de code utilisera ReactNative en parallèle avec la version Web.

Au cœur de notre approche se trouve le principe de l'isolement du code React dans un dossier appelé vues , et du code redux dans un autre dossier appelé redux.

Cette séparation au niveau d'entrée nous donne la flexibilité d'organiser les différentes parties de l'application de manière complètement différente.

Dans le dossier des vuesnous prenons en charge l'organisation des fichiers fonctionnels. Dans le contexte de React, cela semble naturel: pages, mises en page, composants, exhausteurs , etc.

Afin de ne pas devenir fou du nombre de fichiers dans un dossier, vous pouvez utiliser l'approche de fonctionnalité dans ces dossiers.

Pendant ce temps, dans le dossier redux ...

Présentation des re-ducks


Chaque fonction d'application doit correspondre à des actions et des réducteurs distincts, de sorte qu'il est logique d'appliquer l'approche axée sur les fonctionnalités.

L'approche modulaire d' origine des canards , il est facile de travailler avec Redux et offre un moyen structuré pour ajouter de nouvelles fonctionnalités à votre application.

Cependant, vous vouliez comprendre ce qui se passe lorsque vous mettez à l'échelle l'application. Nous avons réalisé que la façon d'organiser un fichier par fonctionnalité encombre l'application et rend son support problématique.

Des canards sont donc apparus . La solution était de diviser la fonctionnalité en dossiers de canard.

duck/
├── actions.js
├── index.js
├── operations.js
├── reducers.js
├── selectors.js
├── tests.js
├── types.js
├── utils.js

Le dossier canard devrait:

  • Contient toute la logique de traitement d'un seul concept de votre application, par exemple: produit, panier, session, etc.
  • Contient le fichier index.js, qui est exportĂ© selon les règles de canard.
  • Stockez le code dans un seul fichier qui fait un travail similaire, comme les rĂ©ducteurs, les sĂ©lecteurs et les actions.
  • Contient des tests liĂ©s au canard.

Par exemple, dans cet exemple, nous n'avons pas utilisé d'abstractions construites au-dessus de redux. Lors de la création d'un logiciel, il est important de commencer avec le moins d'abstraction. Ainsi, vous verrez que la valeur de vos abstractions ne dépasse pas leurs avantages.

Si vous voulez vous assurer que l'abstraction est mauvaise, regardez cette vidéo de Cheng Lou .

Regardons le contenu de chaque fichier.

Les types


Le fichier types contient les noms des actions que vous exécutez dans votre application. Comme bonne pratique, vous devriez essayer de couvrir l'espace de noms correspondant à la fonction à laquelle ils appartiennent. Cette approche vous aidera lors du débogage d'applications complexes.

const QUACK = "app/duck/QUACK";
const SWIM = "app/duck/SWIM";

export default {
    QUACK,
    SWIM
};

Actions


Ce fichier contient toutes les fonctions du créateur d'action .

import types from "./types";

const quack = ( ) => ( {
    type: types.QUACK
} );

const swim = ( distance ) => ( {
    type: types.SWIM,
    payload: {
        distance
    }
} );

export default {
    swim,
    quack
};

Notez que toutes les actions sont représentées par des fonctions, même si elles ne sont pas paramétrées. Une approche cohérente est la priorité la plus élevée pour une grande base de code.

Les opérations


Pour représenter la chaîne d'opérations ( les Opérations ), vous aurez besoin du middleware redux , pour améliorer la fonction de la répartition . Des exemples populaires sont redux-thunk , redux-saga ou redux-observable .

Dans notre cas, le redux-thunk est utilisé . Nous devons séparer les thunks des créateurs d'actions, même au prix d'écrire du code supplémentaire. Par conséquent, nous définirons l'opération comme un wrapper sur les actions .

Si l'opération n'envoie qu'une seule action , c'est-à-dire qu'elle n'utilise pas réellement redux-thunk , nous envoyons la fonction de créateur d'action. Si l'opération utilise du thunk , elle peut envoyer de nombreuses actions et les lier à l'aide de promesses .

import actions from "./actions";

// This is a link to an action defined in actions.js.
const simpleQuack = actions.quack;

// This is a thunk which dispatches multiple actions from actions.js
const complexQuack = ( distance ) => ( dispatch ) => {
    dispatch( actions.quack( ) ).then( ( ) => {
        dispatch( actions.swim( distance ) );
        dispatch( /* any action */ );
    } );
}

export default {
    simpleQuack,
    complexQuack
};

Appelez-les opérations, thunks , sagas , epics , comme vous le souhaitez. Identifiez simplement les principes de dénomination et respectez-les.

À la toute fin, nous parlerons d'index et verrons que les opérations font partie de l'interface publique de canard. Les actions sont encapsulées, les opérations deviennent accessibles de l'extérieur.

RĂ©ducteurs


Si vous avez une fonction plus multiforme, vous devez absolument utiliser plusieurs réducteurs pour gérer des structures d'états complexes. De plus, n'ayez pas peur d'utiliser autant de moissonneuses-batteuses que nécessaire. Cela permettra de travailler plus librement avec les structures d'objets d'état.

import { combineReducers } from "redux";
import types from "./types";

/* State Shape
{
    quacking: bool,
    distance: number
}
*/

const quackReducer = ( state = false, action ) => {
    switch( action.type ) {
        case types.QUACK: return true;
        /* ... */
        default: return state;
    }
}

const distanceReducer = ( state = 0, action ) => {
    switch( action.type ) {
        case types.SWIM: return state + action.payload.distance;
        /* ... */
        default: return state;
    }
}

const reducer = combineReducers( {
    quacking: quackReducer,
    distance: distanceReducer
} );

export default reducer;

Dans une grande application, l'arborescence d'état comprendra au moins trois niveaux. Les fonctions de réduction doivent être aussi petites que possible et gérer uniquement des constructions de données simples. La fonction combineReducers est tout ce dont vous avez besoin pour créer une structure d'état flexible et maintenable.

Consultez l' exemple de projet à part entière et voyez comment utiliser combineReducers correctement , en particulier dans les fichiers reducers.jset store.jsoù nous construisons l' arborescence d'état.

SĂ©lecteurs


Avec le sélecteur d'opérations ( sélecteur ) font partie du canard de l'interface publique. La différence entre les opérations et les sélecteurs est similaire au modèle CQRS .

Les fonctions de sélection prennent une tranche de l'état de l'application et renvoient certaines données en fonction de celui-ci. Ils n'apportent jamais de modifications à l'état de l'application.

function checkIfDuckIsInRange( duck ) {
    return duck.distance > 1000;
}

export default {
    checkIfDuckIsInRange
};

Indice


Ce fichier indique ce qui sera exporté du dossier duck.
Est-il:

  • Exporte le rĂ©ducteur fonction de canard par dĂ©faut.
  • Exporte les sĂ©lecteurs et les opĂ©rations en tant qu'exportations enregistrĂ©es.
  • Exporte les types si nĂ©cessaire dans d'autres canards.

import reducer from "./reducers";

export { default as duckSelectors } from "./selectors";
export { default as duckOperations } from "./operations";
export { default as duckTypes } from "./types";

export default reducer;

Les tests


L'avantage d'utiliser Redux avec le framework ducks est que vous pouvez écrire des tests juste après le code que vous souhaitez tester.

Tester votre code sur Redux est assez simple:

import expect from "expect.js";
import reducer from "./reducers";
import actions from "./actions";

describe( "duck reducer", function( ) {
    describe( "quack", function( ) {
        const quack = actions.quack( );
        const initialState = false;

        const result = reducer( initialState, quack );

        it( "should quack", function( ) {
            expect( result ).to.be( true ) ;
        } );
    } );
} );

Dans ce fichier, vous pouvez écrire des tests pour les réducteurs , les opérations, les sélecteurs, etc.
Je pourrais écrire un article séparé sur les avantages du test de code, mais ils sont déjà suffisants, alors testez simplement votre code!

C'est tout


La bonne nouvelle à propos des re-ducks est que vous pouvez utiliser le même modèle pour tout votre code redux.

L'approche de partitionnement basée sur les fonctionnalités de votre code redux aide votre application à rester flexible et évolutive à mesure qu'elle grandit. Une approche de séparation basée sur les fonctions fonctionnera bien lors de la construction de petits composants communs à différentes parties de l'application.

Vous pouvez jeter un œil à la base de code complète react-redux-example ici . N'oubliez pas que le référentiel fonctionne activement.

Comment organisez-vous vos applications redux? J'ai hâte de recevoir vos commentaires sur l'approche décrite.

Rendez-vous sur le parcours .

All Articles