Redux: Recommandations de bonnes pratiques concernant les créateurs d'actions, les réducteurs et les sélecteurs

Créé le 22 déc. 2015  ·  106Commentaires  ·  Source: reduxjs/redux

Mon équipe utilise Redux depuis quelques mois maintenant. En cours de route, je me suis parfois retrouvé à penser à une fonctionnalité et à me demander "cela appartient-il à un créateur d'action ou à un réducteur ?". La documentation semble un peu vague sur ce fait. (Ou peut-être que j'ai juste manqué où c'est couvert, auquel cas je m'excuse.) partager et discuter avec les autres.

Voici donc mes réflexions.

Utilisez des sélecteurs partout

Ce premier n'est pas strictement lié à Redux mais je le partagerai quand même puisqu'il est indirectement mentionné ci-dessous. Mon équipe utilise rackt/reselect . Nous définissons généralement un fichier qui exporte des sélecteurs pour un nœud donné de notre arbre d'état (par exemple MyPageSelectors). Nos conteneurs « intelligents » utilisent ensuite ces sélecteurs pour paramétrer nos composants « stupides ».

Au fil du temps, nous avons réalisé qu'il y a un avantage supplémentaire à utiliser ces mêmes sélecteurs à d'autres endroits (pas seulement dans le contexte de la resélection). Par exemple, nous les utilisons dans des tests automatisés. Nous les utilisons également dans les thunks renvoyés par les créateurs d'action (plus ci-dessous).

Ma première recommandation est donc d'utiliser des sélecteurs partagés _partout_- même lorsque vous accédez aux données de manière synchrone (par exemple, préférez myValueSelector(state) à state.myValue ). Cela réduit la probabilité de variables mal saisies qui conduisent à des valeurs indéfinies subtiles, cela simplifie les modifications apportées à la structure de votre magasin, etc.

Faites _plus_ dans les créateurs d'action et _moins_ dans les réducteurs

Je pense que celui-ci est très important même s'il n'est peut-être pas immédiatement évident. La logique métier appartient aux créateurs d'action. Les réducteurs doivent être stupides et simples. Dans de nombreux cas individuels, cela n'a pas d'importance, mais la cohérence est bonne et il est donc préférable de le faire _constamment_. Il y a plusieurs raisons pour lesquelles :

  1. Les créateurs d'action peuvent être asynchrones grâce à l'utilisation de middleware comme redux-thunk . Étant donné que votre application nécessitera souvent des mises à jour asynchrones de votre magasin, une "logique métier" se retrouvera dans vos actions.
  2. Les créateurs d'action (plus précisément les thunks qu'ils renvoient) peuvent utiliser des sélecteurs partagés car ils ont accès à l'état complet. Les réducteurs ne peuvent pas car ils n'ont accès qu'à leur nœud.
  3. En utilisant redux-thunk , un seul créateur d'action peut envoyer plusieurs actions, ce qui simplifie les mises à jour d'état compliquées et encourage une meilleure réutilisation du code.

Imaginez que votre état a des métadonnées liées à une liste d'éléments. Chaque fois qu'un élément est modifié, ajouté ou supprimé de la liste, les métadonnées doivent être mises à jour. La "logique métier" permettant de synchroniser la liste et ses métadonnées pourrait résider à plusieurs endroits :

  1. Dans les réducteurs. Chaque réducteur (ajouter, éditer, supprimer) est responsable de la mise à jour de la liste _ainsi que_ des métadonnées.
  2. Dans les vues (conteneur/composant). Chaque vue qui invoque une action (ajouter, modifier, supprimer) est également responsable d'invoquer une action updateMetadata . Cette approche est terrible pour (espérons-le) des raisons évidentes.
  3. Dans l'action-créateurs. Chaque créateur d'action (ajouter, modifier, supprimer) renvoie un thunk qui envoie une action pour mettre à jour la liste, puis une autre action pour mettre à jour les métadonnées.

Compte tenu des choix ci-dessus, l'option 3 est nettement meilleure. Les options 1 et 3 prennent en charge le partage de code propre, mais seule l'option 3 prend en charge le cas où les mises à jour de la liste et/ou des métadonnées peuvent être asynchrones. (Par exemple, cela repose peut-être sur un travailleur Web.)

Écrire des tests « canards » qui se concentrent sur les actions et les sélecteurs

Le moyen le plus efficace de tester les actions, les réducteurs et les sélecteurs est de suivre l'approche « canards » lors de l'écriture des tests. Cela signifie que vous devez écrire un ensemble de tests qui couvre un ensemble donné d'actions, de réducteurs et de sélecteurs plutôt que 3 ensembles de tests qui se concentrent sur chacun individuellement. Cela simule plus précisément ce qui se passe dans votre application réelle et offre le meilleur rapport qualité-prix.

En le décomposant davantage, j'ai trouvé qu'il est utile d'écrire des tests qui se concentrent sur les créateurs d'action, puis de vérifier le résultat à l'aide de sélecteurs. (Ne testez pas directement les réducteurs.) Ce qui compte, c'est qu'une action donnée aboutisse à l'état que vous attendez. Vérifier ce résultat à l'aide de vos sélecteurs (partagés) est un moyen de couvrir les trois en un seul passage.

discussion

Commentaire le plus utile

@dtinth @denis-sokolov Je suis aussi d'accord avec toi là-dessus. D'ailleurs, lorsque je faisais référence au projet redux-saga , je n'ai peut-être pas clairement indiqué que je suis contre l'idée de faire grandir les actionCreators et de les rendre de plus en plus complexes au fil du temps.

Le projet Redux-saga est également une tentative de faire ce que vous décrivez @dtinth mais il y a une différence subtile avec ce que vous dites tous les deux. Il semble que vous vouliez dire que si vous écrivez chaque événement brut qui s'est produit dans le journal d'action, vous pouvez facilement calculer n'importe quel état à partir des réducteurs de ce journal d'action. C'est absolument vrai, et j'ai emprunté cette voie pendant un certain temps jusqu'à ce que mon application devienne très difficile à maintenir car le journal des actions n'est pas devenu explicite et les réducteurs trop complexes au fil du temps.

Peut-être que vous pouvez regarder ce point de la discussion originale qui a conduit à la discussion sur la saga Redux : https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Cas d'utilisation à résoudre

Imaginez que vous ayez une application Todo, avec l'événement évident TodoCreated. Ensuite, nous vous demandons de coder une application d'intégration. Une fois que l'utilisateur a créé une tâche, nous devons le féliciter avec une fenêtre contextuelle.

La voie "impure":

C'est ce que @bvaughn semble préférer

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

Je n'aime pas cette approche car elle rend le créateur d'action fortement couplé à la disposition de la vue de l'application. Il suppose que l'actionCreator doit connaître la structure de l'arbre d'état de l'interface utilisateur pour prendre sa décision.

La méthode « tout calculer à partir d'événements bruts » :

C'est ce que @denis-sokolov @dtinth semble préférer :

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Oui, vous pouvez créer un réducteur qui sait si la félicitation doit être affichée. Mais alors vous avez une fenêtre contextuelle qui s'affichera sans même une action indiquant que la fenêtre contextuelle a été affichée. D'après ma propre expérience en faisant cela (et j'ai toujours du code hérité qui le fait), il est toujours préférable de le rendre très explicite : ne JAMAIS afficher la fenêtre contextuelle de félicitations si aucune action DISPLAY_CONGRATULATION n'est déclenchée. Explicite est beaucoup plus facile à maintenir qu'implicite.

La manière simplifiée de la saga.

La saga redux utilise des générateurs et peut sembler un peu compliquée si vous n'y êtes pas habitué, mais en gros, avec une implémentation simplifiée, vous écririez quelque chose comme :

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

La saga est un acteur avec état qui reçoit des événements et peut produire des effets. Ici, il est implémenté comme un réducteur impur pour vous donner une idée de ce que c'est, mais ce n'est en fait pas dans le projet redux-saga.

Pour compliquer un peu les règles :

Si vous faites attention à la règle initiale elle n'est pas très explicite sur tout.
Si vous regardez les implémentations ci-dessus, vous remarquerez que la fenêtre contextuelle de félicitations s'ouvre à chaque fois que nous créons une tâche pendant l'intégration. Très probablement, nous voulons qu'il ne s'ouvre que pour la première tâche créée qui se produit lors de l'intégration et pas pour toutes. De plus, nous voulons permettre à l'utilisateur de refaire éventuellement l'intégration depuis le début.

Pouvez-vous voir comment le code deviendrait désordonné dans les 3 implémentations au fil du temps alors que l'intégration devient de plus en plus compliquée ?

La manière redux-saga

Avec redux-saga et les règles d'intégration ci-dessus, vous écririez quelque chose comme

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Je pense que cela résout ce cas d'utilisation d'une manière beaucoup plus simple que les solutions ci-dessus. Si je me trompe s'il vous plaît donnez-moi votre implémentation plus simple :)

Vous avez parlé de code impur, et dans ce cas, il n'y a pas d'impureté dans l'implémentation de Redux-saga car les effets take/put sont en fait des données. Lorsque take() est appelé, il ne s'exécute pas, il renvoie un descripteur de l'effet à exécuter et, à un moment donné, un interpréteur se déclenche, vous n'avez donc pas besoin de simulation pour tester les sagas. Si vous êtes un développeur fonctionnel utilisant Haskell, pensez aux monades Free / IO.


Dans ce cas, il permet de :

  • Évitez de compliquer actionCreator et faites-le dépendre de getState
  • Rendre l'implicite plus explicite
  • Evitez de coupler une logique transversale (comme l'onboarding ci-dessus) à votre cœur de métier (créer des tâches)

Il peut également fournir une couche d'interprétation, permettant de traduire des événements bruts en événements plus significatifs/de haut niveau (un peu comme le fait ELM en enveloppant les événements au fur et à mesure qu'ils bouillonnent).

Exemples:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" pourrait conduire à "NEXT_PAGE_LOADED"
  • "REQUEST_FAILED" peut conduire à "USER_DISCONNECTED" si le code d'erreur est 401.
  • "HASHTAG_FILTER_ADDED" pourrait conduire à "CONTENT_RELOADED"

Si vous souhaitez réaliser une disposition d'application modulaire avec des canards, cela peut permettre d'éviter de coupler les canards ensemble. La saga devient le point de couplage. Les canards n'ont qu'à connaître leurs événements bruts, et la saga interprète ces événements bruts. C'est bien mieux que d'avoir duck1 expédiant directement les actions de duck2 car cela rend le projet duck1 plus facile à réutiliser dans un autre contexte. On pourrait cependant affirmer que le point de couplage pourrait également être dans l'actionCréateurs et c'est ce que la plupart des gens font aujourd'hui.

Tous les 106 commentaires

Curieux si vous utilisez Immutable.js ou autre. Dans la poignée de choses redux que j'ai construites, je ne pouvais pas imaginer ne pas utiliser immutable, mais j'ai une structure assez profondément imbriquée qu'Immutable aide à apprivoiser.

Wow. Quel oubli pour moi _not_ de mentionner cela. Oui! Nous utilisons Immuable ! Comme vous le dites, il est difficile d'imaginer _pas_ l'utiliser pour quelque chose de substantiel.

@bvaughn Un domaine avec lequel j'ai eu du mal est de tracer la ligne entre Immutable et les composants. Passer des objets immuables dans les composants vous permet d'utiliser très facilement des décorateurs/mixins de rendu pur, mais vous vous retrouvez ensuite avec du code IMmutable dans vos composants (ce que je n'aime pas). Jusqu'à présent, je viens de céder et de le faire, mais je soupçonne que vous utilisez des sélecteurs dans les méthodes render() au lieu d'accéder directement aux méthodes d'Immutable.js?

Pour être honnête, c'est quelque chose sur lequel nous n'avons pas encore défini de politique stricte. Souvent, nous utilisons des sélecteurs dans nos conteneurs "intelligents" pour extraire des valeurs natives de nos objets immuables, puis nous transmettons les valeurs natives à nos composants sous forme de chaînes, de booléens, etc. passer un type Record pour que le composant puisse le traiter comme un objet natif (avec des getters).

J'ai évolué dans la direction opposée, rendant les créateurs d'action plus triviaux. Mais je viens juste de commencer avec le redux. Quelques questions sur votre approche :

1) Comment testez-vous vos créateurs d'action ? J'aime déplacer autant de logique que possible vers des fonctions pures et synchrones qui ne dépendent pas de services externes car c'est plus facile à tester.
2) Utilisez-vous le voyage dans le temps avec rechargement à chaud ? L'une des choses intéressantes avec les devtools react redux est que lorsque le rechargement à chaud est configuré, le magasin réexécute toutes les actions contre le nouveau réducteur. Si je devais déplacer ma logique dans les créateurs d'action, je perdrais cela.
3) Si vos créateurs d'action envoient plusieurs fois pour provoquer un effet, cela signifie-t-il que votre état est brièvement dans un état invalide ? (Je pense ici à plusieurs envois synchrones, pas à des envois asynchrone à un moment ultérieur)

Utilisez des sélecteurs partout

Oui, cela semble dire que vos réducteurs sont un détail d'implémentation de votre état et que vous exposez votre état à votre composant via une API de requête.
Comme toute interface, elle permet de découpler et de faciliter la refactorisation de l'état.

Utiliser ImmutableJS

IMO avec la nouvelle syntaxe JS, il n'est plus tellement utile d'utiliser ImmutableJS car vous pouvez facilement modifier des listes et des objets avec JS normal. À moins que vous n'ayez de très grandes listes et objets avec de nombreuses propriétés et que vous ayez besoin d'un partage structurel pour des raisons de performances, ImmutableJS n'est pas une exigence stricte.

Faire plus en actionCréateurs

@bvaughn tu devrais vraiment regarder ce projet : https://github.com/yelouafi/redux-saga
Quand j'ai commencé à discuter de sagas (initialement concept backend) avec @yelouafi c'était pour résoudre ce genre de problème. Dans mon cas, j'ai d'abord essayé d'utiliser des sagas tout en connectant un utilisateur à une application existante.

1) Comment testez-vous vos créateurs d'action ? J'aime déplacer autant de logique que possible vers des fonctions pures et synchrones qui ne dépendent pas de services externes car c'est plus facile à tester.

J'ai essayé de décrire cela ci-dessus, mais fondamentalement... Je pense qu'il est plus logique (pour moi jusqu'à présent) de tester vos créateurs d'action avec une approche de type "canards". Commencez un test en envoyant le résultat d'un créateur d'action, puis vérifiez l'état à l'aide de sélecteurs. De cette façon, avec un seul test, vous pouvez couvrir le créateur d'action, son ou ses réducteurs et tous les sélecteurs associés.

2) Utilisez-vous le voyage dans le temps avec rechargement à chaud ? L'une des choses intéressantes avec les devtools react redux est que lorsque le rechargement à chaud est configuré, le magasin réexécute toutes les actions contre le nouveau réducteur. Si je devais déplacer ma logique dans les créateurs d'action, je perdrais cela.

Non, nous n'utilisons pas le voyage dans le temps. Mais pourquoi votre logique métier d'être dans un créateur d'action aurait-elle un impact ici ? La seule chose qui met à jour l'état de votre application, ce sont vos réducteurs. Et donc, réexécuter les actions créées obtiendrait le même résultat dans les deux cas.

3) Si vos créateurs d'action envoient plusieurs fois pour provoquer un effet, cela signifie-t-il que votre état est brièvement dans un état invalide ? (Je pense ici à plusieurs envois synchrones, pas à des envois asynchrone à un moment ultérieur)

L'état invalide transitoire est quelque chose que vous ne pouvez pas vraiment éviter dans certains cas. Tant qu'il y a une cohérence éventuelle, ce n'est généralement pas un problème. Et encore une fois, votre état pourrait être temporairement invalide, quelle que soit votre logique métier dans les créateurs d'action ou les réducteurs. Cela a plus à voir avec les effets secondaires et les spécificités de votre magasin.

IMO avec la nouvelle syntaxe JS, il n'est plus tellement utile d'utiliser ImmutableJS car vous pouvez facilement modifier des listes et des objets avec JS normal. À moins que vous n'ayez de très grandes listes et objets avec de nombreuses propriétés et que vous ayez besoin d'un partage structurel pour des raisons de performances, ImmutableJS n'est pas une exigence stricte.

Les principales raisons d'utiliser Immutable (à mes yeux) ne sont pas les performances ou le sucre syntaxique pour les mises à jour. La raison principale est que cela vous empêche (ou quelqu'un d'autre) de muter _accidentellement_ votre état entrant dans un réducteur. C'est un non-non et c'est malheureusement facile à faire avec des objets JS simples.

@bvaughn tu devrais vraiment regarder ce projet : https://github.com/yelouafi/redux-saga
Quand j'ai commencé à discuter de sagas (initialement concept backend) avec @yelouafi c'était pour résoudre ce genre de problème. Dans mon cas, j'ai d'abord essayé d'utiliser des sagas tout en connectant un utilisateur à une application existante.

J'ai déjà vérifié ce projet auparavant :) Bien que je ne l'aie pas encore utilisé. Ça a l'air soigné.

J'ai essayé de décrire cela ci-dessus, mais fondamentalement... Je pense qu'il est plus logique (pour moi jusqu'à présent) de tester vos créateurs d'action avec une approche de type "canards". Commencez un test en envoyant le résultat d'un créateur d'action, puis vérifiez l'état à l'aide de sélecteurs. De cette façon, avec un seul test, vous pouvez couvrir le créateur d'action, son ou ses réducteurs et tous les sélecteurs associés.

Désolé, j'ai cette partie. Ce que je me demandais, c'est la partie des tests qui interagit avec l'asynchronicité. Je pourrais écrire un test comme ceci :

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

Mais à quoi ressemble votre test ? Quelque chose comme ça?

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

Non, nous n'utilisons pas le voyage dans le temps. Mais pourquoi votre logique métier d'être dans un créateur d'action aurait-elle un impact ici ? La seule chose qui met à jour l'état de votre application, ce sont vos réducteurs. Et donc, réexécuter les actions créées obtiendrait le même résultat dans les deux cas.

Et si le code était modifié ? Si je change de réducteur, les mêmes actions sont rejouées mais avec le nouveau réducteur. Alors que si je change le créateur d'action, les nouvelles versions ne sont pas rejouées. Considérons donc deux scénarios :

Avec un réducteur :

1) J'essaye une action dans mon application.
2) Il y a un bug dans mon réducteur, ce qui entraîne un mauvais état.
3) Je corrige le bug dans le réducteur et enregistre
4) Le voyage dans le temps charge le nouveau réducteur et me met dans l'état dans lequel j'aurais dû être.

Alors qu'avec un créateur d'action

1) J'essaye une action dans mon application.
2) Il y a un bogue dans le créateur d'action, ce qui entraîne la création d'une mauvaise action
3) Je corrige le bug dans le créateur d'action et enregistre
4) Je suis toujours dans un état incorrect, ce qui m'obligera au moins à réessayer l'action, et éventuellement à actualiser si cela me met dans un état complètement cassé.

L'état invalide transitoire est quelque chose que vous ne pouvez pas vraiment éviter dans certains cas. Tant qu'il y a une cohérence éventuelle, ce n'est généralement pas un problème. Et encore une fois, votre état pourrait être temporairement invalide, quelle que soit votre logique métier dans les créateurs d'action ou les réducteurs. Cela a plus à voir avec les effets secondaires et les spécificités de votre magasin.

Je suppose que ma façon de penser au redux insiste sur le fait que le magasin est toujours dans un état valide. Le réducteur prend toujours un état valide et produit un état valide. Selon vous, quels cas obligent à autoriser certains états incohérents ?

Désolé pour l'interruption, mais qu'entendez-vous par états invalides et valides
ici? Données en cours de chargement ou action en cours d'exécution mais aspect pas encore terminé
comme un état transitoire valide pour moi.

Qu'entendez-vous par état transitoire dans Redux @bvaughn et @sompylasar ? Soit l'envoi se termine, soit il se lance. S'il jette alors l'état ne change pas.

À moins que votre réducteur n'ait des problèmes de code, Redux n'a que des états cohérents avec la logique du réducteur. D'une manière ou d'une autre, toutes les actions envoyées sont gérées dans une transaction : que l'ensemble de l'arbre soit mis à jour ou que l'état ne change pas du tout.

Si l'ensemble de l'arborescence se met à jour mais pas de manière appropriée (comme un état que React ne peut pas rendre), c'est simplement que vous n'avez pas fait votre travail correctement :)

Dans Redux, l'état actuel consiste à considérer qu'un seul envoi est une frontière de transaction.

Cependant je comprends l'inquiétude de @winstonewert qui semble vouloir envoyer 2 actions de manière synchrone dans une même transaction. Parce que parfois actionCreators envoie plusieurs actions et s'attend à ce que toutes les actions soient exécutées correctement. Si 2 actions sont envoyées et que la seconde échoue, alors seule la 1ère sera appliquée, conduisant à un état que nous pourrions considérer comme "incohérent". Peut-être que @winstonewert veut que si la deuxième action échoue, nous annulons les 2 actions.

@winstonewert J'ai implémenté quelque chose comme ça dans notre cadre interne ici et cela fonctionne bien jusqu'à présent : https://github.com/stample/atom-react/blob/master/src/atom/atom.js
Je voulais également gérer les erreurs de rendu : si un état ne peut pas être rendu avec succès, je voulais que mon état soit annulé pour éviter de bloquer l'interface utilisateur. Malheureusement, jusqu'à la prochaine version, React fait un très mauvais travail lorsque les méthodes de rendu génèrent des erreurs, ce n'était donc pas très utile mais pourrait l'être à l'avenir.

Je suis presque sûr que nous pouvons permettre à un magasin d'accepter plusieurs envois de synchronisation dans une transaction avec un middleware.

Cependant, je ne suis pas sûr qu'il soit possible de restaurer l'état en cas d'erreur de rendu, car généralement le magasin de redux s'est déjà "engagé" lorsque nous essayons de rendre son état. Dans mon framework, il existe un hook "beforeTransactionCommit" que j'utilise pour déclencher le rendu et pour éventuellement revenir en arrière sur toute erreur de rendu.

@gaearon Je me demande si vous envisagez de prendre en charge ce type de fonctionnalités et si cela serait possible avec l'API actuelle.

Il me semble que redux-batched-subscribe ne permet pas de faire de vraie transaction mais juste de réduire le nombre de rendus. Ce que je vois, c'est que le store "s'engage" après chaque envoi même si l'écouteur d'abonnement n'est déclenché qu'une seule fois à la fin

Pourquoi avons-nous besoin d'un support transactionnel complet ? Je ne pense pas comprendre le cas d'utilisation.

@gaearon Je ne suis pas encore vraiment sûr, mais je serais heureux d'en savoir plus sur le cas d'

L'idée est que vous pouvez faire dispatch([a1,a2]) et si a2 échoue, alors nous revenons à l'état avant l'envoi de a1.

Dans le passé, j'ai souvent envoyé plusieurs actions de manière synchrone (sur un seul écouteur onClick par exemple, ou dans un actionCreator) et j'ai principalement implémenté des transactions comme moyen d'appeler le rendu uniquement à la fin de toutes les actions envoyées, mais cela a été résolu d'une manière différente par le projet redux-batched-subscribe.

Dans mes cas d'utilisation, les actions que j'avais l'habitude de déclencher sur une transaction visaient principalement à éviter les rendus inutiles, mais les actions avaient un sens indépendamment, donc même si l'envoi échouait pour la 2ème action, ne pas annuler la 1ère action me donnerait toujours un état cohérent ( mais peut-être pas celui qui était prévu...). Je ne sais pas vraiment si quelqu'un peut proposer un cas d'utilisation où une restauration complète serait utile

Cependant, lorsque le rendu échoue, n'est-il pas logique d'essayer de revenir au dernier état pour lequel le rendu n'échoue pas au lieu d'essayer de progresser sur un état non rendu ?

Est-ce qu'un simple amplificateur de réduction fonctionnerait? par exemple

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

Avec cela, vous pouvez envoyer un tableau au magasin. L'activateur déballe l'action individuelle et la transmet à chaque réducteur.

Ohh @gaearon je ne le savais pas. Je n'avais pas remarqué qu'il y avait 2 projets distincts qui essayaient de résoudre un cas d'utilisation assez similaire de différentes manières :

Les deux permettront d'éviter les rendus inutiles, mais le 1er annulerait toutes les actions par lots tandis que le 2ème n'appliquerait que l'action défaillante.

@gaearon Ouch, mon mal de ne pas avoir regardé ça. :vidé:


Les créateurs d'action représentent le code impur

Je n'ai probablement pas eu autant d'expérience pratique avec Redux que la plupart des gens, mais à première vue, je ne suis pas d'accord avec "faire plus de créateurs en action et moins de réducteurs", j'ai eu une discussion similaire au sein de notre entreprise.

Dans Hacker Way: Rethinking Web App Development sur Facebook où le modèle Flux est introduit, le problème même qui conduit à l'invention de Flux est le code impératif.

Dans ce cas, un créateur d'action qui effectue des E/S est ce code impératif.

Nous n'utilisons pas Redux au travail, mais là où je travaille, nous avions l'habitude d'avoir des actions à grain fin (qui, bien sûr, ont toutes un sens en elles-mêmes) et de les déclencher dans un lot. Par exemple, lorsque vous cliquez sur un message, trois actions sont déclenchées : OPEN_MESSAGE_VIEW , FETCH_MESSAGE , MARK_NOTIFICATION_AS_READ .

Ensuite, il s'avère que ces actions de "bas niveau" ne sont rien de plus qu'une "commande" ou "setter" ou "message" pour définir une valeur à l'intérieur d'un magasin. Nous pourrions aussi bien revenir en arrière et utiliser MVC et nous retrouver avec un code plus simple si nous continuons à le faire comme ça.

Dans un sens, les créateurs d'action représentent le code impur, tandis que les réducteurs (et les sélecteurs) représentent le code pur . Les gens d'Haskell ont compris qu'il vaut mieux avoir moins de code impur et plus de code pur .

Par exemple, dans mon projet parallèle (utilisant Redux), j'utilise l'API de reconnaissance vocale de webkit. Il émet onresult événement

  • Demandez au créateur de l'action de traiter l'événement en premier, puis de l'envoyer dans le magasin.
  • Envoyez simplement l'objet événement dans le magasin.

J'ai opté pour le numéro deux : envoyez simplement l'objet d'événement brut dans le magasin.

Cependant, il semble que les outils de développement Redux ne l'aiment pas lorsque des objets non simples sont envoyés dans le magasin, j'ai donc ajouté une petite logique dans le créateur d'action pour transformer ces objets d'événement en objets simples. (Le code dans le créateur d'action est si trivial qu'il ne peut pas se tromper.)

Ensuite, le réducteur peut combiner ces événements très primitifs et construire la transcription de ce qui est dit. Parce que cette logique vit dans du code pur, je peux très facilement la modifier en direct (en rechargeant à chaud le réducteur).

J'aimerais soutenir @dtinth. Les actions doivent représenter des événements qui se sont produits dans le monde réel, et non la façon dont nous voulons réagir à ces événements. En particulier, voir CQRS : nous voulons enregistrer autant de détails sur les événements de la vie réelle, et probablement les réducteurs seront améliorés à l'avenir et traiteront les anciens événements avec une nouvelle logique.

@dtinth @denis-sokolov Je suis aussi d'accord avec toi là-dessus. D'ailleurs, lorsque je faisais référence au projet redux-saga , je n'ai peut-être pas clairement indiqué que je suis contre l'idée de faire grandir les actionCreators et de les rendre de plus en plus complexes au fil du temps.

Le projet Redux-saga est également une tentative de faire ce que vous décrivez @dtinth mais il y a une différence subtile avec ce que vous dites tous les deux. Il semble que vous vouliez dire que si vous écrivez chaque événement brut qui s'est produit dans le journal d'action, vous pouvez facilement calculer n'importe quel état à partir des réducteurs de ce journal d'action. C'est absolument vrai, et j'ai emprunté cette voie pendant un certain temps jusqu'à ce que mon application devienne très difficile à maintenir car le journal des actions n'est pas devenu explicite et les réducteurs trop complexes au fil du temps.

Peut-être que vous pouvez regarder ce point de la discussion originale qui a conduit à la discussion sur la saga Redux : https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Cas d'utilisation à résoudre

Imaginez que vous ayez une application Todo, avec l'événement évident TodoCreated. Ensuite, nous vous demandons de coder une application d'intégration. Une fois que l'utilisateur a créé une tâche, nous devons le féliciter avec une fenêtre contextuelle.

La voie "impure":

C'est ce que @bvaughn semble préférer

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

Je n'aime pas cette approche car elle rend le créateur d'action fortement couplé à la disposition de la vue de l'application. Il suppose que l'actionCreator doit connaître la structure de l'arbre d'état de l'interface utilisateur pour prendre sa décision.

La méthode « tout calculer à partir d'événements bruts » :

C'est ce que @denis-sokolov @dtinth semble préférer :

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Oui, vous pouvez créer un réducteur qui sait si la félicitation doit être affichée. Mais alors vous avez une fenêtre contextuelle qui s'affichera sans même une action indiquant que la fenêtre contextuelle a été affichée. D'après ma propre expérience en faisant cela (et j'ai toujours du code hérité qui le fait), il est toujours préférable de le rendre très explicite : ne JAMAIS afficher la fenêtre contextuelle de félicitations si aucune action DISPLAY_CONGRATULATION n'est déclenchée. Explicite est beaucoup plus facile à maintenir qu'implicite.

La manière simplifiée de la saga.

La saga redux utilise des générateurs et peut sembler un peu compliquée si vous n'y êtes pas habitué, mais en gros, avec une implémentation simplifiée, vous écririez quelque chose comme :

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

La saga est un acteur avec état qui reçoit des événements et peut produire des effets. Ici, il est implémenté comme un réducteur impur pour vous donner une idée de ce que c'est, mais ce n'est en fait pas dans le projet redux-saga.

Pour compliquer un peu les règles :

Si vous faites attention à la règle initiale elle n'est pas très explicite sur tout.
Si vous regardez les implémentations ci-dessus, vous remarquerez que la fenêtre contextuelle de félicitations s'ouvre à chaque fois que nous créons une tâche pendant l'intégration. Très probablement, nous voulons qu'il ne s'ouvre que pour la première tâche créée qui se produit lors de l'intégration et pas pour toutes. De plus, nous voulons permettre à l'utilisateur de refaire éventuellement l'intégration depuis le début.

Pouvez-vous voir comment le code deviendrait désordonné dans les 3 implémentations au fil du temps alors que l'intégration devient de plus en plus compliquée ?

La manière redux-saga

Avec redux-saga et les règles d'intégration ci-dessus, vous écririez quelque chose comme

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Je pense que cela résout ce cas d'utilisation d'une manière beaucoup plus simple que les solutions ci-dessus. Si je me trompe s'il vous plaît donnez-moi votre implémentation plus simple :)

Vous avez parlé de code impur, et dans ce cas, il n'y a pas d'impureté dans l'implémentation de Redux-saga car les effets take/put sont en fait des données. Lorsque take() est appelé, il ne s'exécute pas, il renvoie un descripteur de l'effet à exécuter et, à un moment donné, un interpréteur se déclenche, vous n'avez donc pas besoin de simulation pour tester les sagas. Si vous êtes un développeur fonctionnel utilisant Haskell, pensez aux monades Free / IO.


Dans ce cas, il permet de :

  • Évitez de compliquer actionCreator et faites-le dépendre de getState
  • Rendre l'implicite plus explicite
  • Evitez de coupler une logique transversale (comme l'onboarding ci-dessus) à votre cœur de métier (créer des tâches)

Il peut également fournir une couche d'interprétation, permettant de traduire des événements bruts en événements plus significatifs/de haut niveau (un peu comme le fait ELM en enveloppant les événements au fur et à mesure qu'ils bouillonnent).

Exemples:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" pourrait conduire à "NEXT_PAGE_LOADED"
  • "REQUEST_FAILED" peut conduire à "USER_DISCONNECTED" si le code d'erreur est 401.
  • "HASHTAG_FILTER_ADDED" pourrait conduire à "CONTENT_RELOADED"

Si vous souhaitez réaliser une disposition d'application modulaire avec des canards, cela peut permettre d'éviter de coupler les canards ensemble. La saga devient le point de couplage. Les canards n'ont qu'à connaître leurs événements bruts, et la saga interprète ces événements bruts. C'est bien mieux que d'avoir duck1 expédiant directement les actions de duck2 car cela rend le projet duck1 plus facile à réutiliser dans un autre contexte. On pourrait cependant affirmer que le point de couplage pourrait également être dans l'actionCréateurs et c'est ce que la plupart des gens font aujourd'hui.

@slorber C'est un excellent exemple ! Merci d'avoir pris le temps d'expliquer clairement les avantages et les inconvénients de chaque approche. (Je pense même que ça devrait aller dans la doc.)

J'avais l'habitude d'explorer une idée similaire (que j'ai nommée « composants de travail »). Fondamentalement, c'est un composant React qui ne rend rien ( render: () => null ), mais écoute les événements (par exemple des magasins) et déclenche d'autres effets secondaires. Ce composant de travail est ensuite placé dans le composant racine de l'application. Juste une autre façon folle de gérer les effets secondaires complexes. :stuck_out_tongue:

Beaucoup de discussions ici pendant que je dormais.

@winstonewert , vous soulevez un bon point sur le voyage dans le temps et la relecture du code buggy. Je pense que certains types de bogues/changements ne fonctionneront pas avec le temps de voyage de toute façon, mais je pense que dans l'ensemble vous avez raison.

@dtinth , je suis désolé, mais je ne suis pas la plupart de vos commentaires. Une partie de votre code "canards" de créateur/réducteur d'action doit être impur, en ce sens qu'une partie doit récupérer des données. Au-delà de ça, tu m'as perdu. L'un des principaux objectifs de mon message initial n'était qu'un objectif de pragmatisme.


@winstonewert a déclaré: "Je suppose que ma façon de penser au redux insiste sur le fait que le magasin est toujours dans un état valide."
@slorber a demandé : "Qu'entendez-vous par état transitoire dans Redux @bvaughn et @sompylasar ? Que l'envoi se termine ou qu'il se lance. S'il se lance, l'état ne change pas."

Je suis presque sûr que nous pensons à des choses différentes. Quand j'ai dit "état non valide transitoire", je faisais référence à un cas d'utilisation comme le suivant. Par exemple, moi-même et un collègue avons récemment publié

Imaginez que votre magasin d'applications contienne quelques objets interrogeables : [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] . L'utilisateur a entré le texte de filtre "e" et donc le middleware de recherche contient un tableau d'identifiants 1 et 3. Imaginez maintenant que l'utilisateur 1 (Alex) est supprimé - soit en réponse à une action de l'utilisateur localement, soit à une actualisation des données distantes qui ne contient plus cet enregistrement d'utilisateur. Au moment où votre réducteur met à jour la collection des utilisateurs, votre magasin sera temporairement invalide, car redux-search référencera un identifiant qui n'existe plus dans la collection. Une fois le middleware exécuté à nouveau, il corrigera l'état invalide. Ce genre de chose peut arriver chaque fois qu'un nœud de votre arbre est lié à un autre nœud.


@slorber a déclaré: "Je n'aime pas cette approche car elle rend le créateur d'action fortement couplé à la disposition de la vue de l'application. Cela suppose que l'actionCreator doit connaître la structure de l'arbre d'état de l'interface utilisateur pour prendre sa décision."

Je ne comprends pas ce que vous entendez par l'approche couplant le créateur d'action "à la disposition de la vue de l'application". L'arbre d'état _drives_ (ou informe) l'UI. C'est l'un des principaux objectifs de Flux. Et vos créateurs et réducteurs d'action sont, par définition, couplés à cet état (mais pas à l'interface utilisateur).

Pour ce que ça vaut, l'exemple de code que vous avez écrit comme quelque chose que je préfère n'est pas le genre de chose que j'avais en tête. Peut-être que je me suis mal expliqué. Je pense qu'une difficulté à discuter de quelque chose comme ça est que cela ne se manifeste généralement pas dans des exemples simples ou courants. (Par exemple, l'application TODO MVC standard n'est pas assez complexe pour des discussions nuancées comme celle-ci.)

Edité pour plus de clarté sur le dernier point.

Btw @slorber voici un exemple de ce que j'avais en tête. C'est un peu artificiel.

Disons que votre état a de nombreux nœuds. L'un de ces nœuds stocke les ressources partagées. (Par "partagé", j'entends les ressources mises en cache localement et accessibles par plusieurs pages de votre application.) Ces ressources partagées ont leurs propres créateurs et réducteurs d'action ("canards"). Un autre nœud stocke des informations pour une page d'application particulière. Votre page a également son propre canard.

Disons que votre page devait charger la dernière et la meilleure chose, puis permettre à un utilisateur de la modifier. Voici un exemple d'approche de créateur d'action que je pourrais utiliser pour une telle situation :

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

Peut-être que @winstonewert veut que si la deuxième action échoue, nous annulons les 2 actions.

Non. Je n'écrirais pas un créateur d'action qui envoie deux actions. Je définirais une seule action qui a fait deux choses. L'OP semble préférer les créateurs d'action qui envoient des actions plus petites qui permettent les états invalides transitoires que je n'aime pas.

Au moment où votre réducteur met à jour la collection des utilisateurs, votre magasin sera temporairement invalide, car redux-search référencera un identifiant qui n'existe plus dans la collection. Une fois le middleware exécuté à nouveau, il corrigera l'état invalide. Ce genre de chose peut arriver chaque fois qu'un nœud de votre arbre est lié à un autre nœud.

C'est effectivement le genre de cas qui me dérange. À mon avis, l'index serait idéalement quelque chose géré entièrement par le réducteur ou un sélecteur. Devoir envoyer des actions supplémentaires pour maintenir la recherche à jour semble une utilisation moins pure du redux.

L'OP semble préférer les créateurs d'action qui envoient des actions plus petites qui permettent les états invalides transitoires que je n'aime pas.

Pas exactement. Je privilégierais les actions uniques en ce qui concerne le nœud de votre créateur d'action de l'arbre d'état. Mais si une seule "action" utilisateur conceptuelle affecte plusieurs nœuds de l'arbre d'état, vous devrez alors envoyer plusieurs actions. Vous pouvez invoquer séparément chaque action (ce qui, je pense, est _mauvais_) ou vous pouvez avoir un seul créateur d'action qui envoie les actions (la méthode redux-thunk, qui, je pense, est _meilleure_ car elle cache ces informations de votre couche de vue).

C'est effectivement le genre de cas qui me dérange. À mon avis, l'index serait idéalement quelque chose géré entièrement par le réducteur ou un sélecteur. Devoir envoyer des actions supplémentaires pour maintenir la recherche à jour semble une utilisation moins pure du redux.

Vous n'envoyez pas d'actions supplémentaires. La recherche est un middleware. C'est automatique. Mais il existe un état transitoire lorsque les deux nœuds de votre arbre ne concordent pas.

@bvaughn Oh, désolé d'être un puriste !

Eh bien, le code impur a à voir avec la récupération de données et d'autres effets secondaires/IO, alors que le code pur ne peut déclencher aucun effet secondaire. Voir ce tableau pour la comparaison entre le code pur et impur.

Les bonnes pratiques de Flux disent qu'une action doit « décrire l'action d'un utilisateur, ne sont pas des setters ». Les documents de Flux ont également laissé entendre d'où ces actions sont censées provenir :

Lorsque de nouvelles données entrent dans le système, que ce soit via une personne interagissant avec l'application ou via un appel d'API Web , ces données sont regroupées dans une action - un littéral d'objet contenant les nouveaux champs de données et un type d'action spécifique.

Fondamentalement, les actions sont des faits/données qui décrivent « ce qui s'est passé », pas ce qui devrait se produire. Les magasins ne peuvent réagir à ces actions que de manière synchrone, prévisible et sans aucun autre effet secondaire. Tous les autres effets secondaires doivent être traités dans les créateurs d'action (ou sagas :wink:).

Exemple

Je ne dis pas que c'est le meilleur moyen ou meilleur que n'importe quel autre moyen, ou même un bon moyen. Mais c'est ce que je considère actuellement comme la meilleure pratique.

Par exemple, supposons que l'utilisateur souhaite afficher le tableau de bord qui nécessite une connexion à un serveur distant. Voici ce qui devrait se passer :

  • L'utilisateur clique sur le bouton Afficher le tableau de bord.
  • La vue du tableau de bord s'affiche avec un indicateur de chargement.
  • Une requête est envoyée au serveur pour récupérer le tableau de bord.
  • Attendez la réponse.
  • En cas de succès, affichez le tableau de bord.
  • En cas d'échec, le tableau de bord se ferme et une boîte de message apparaît avec un message d'erreur. L'utilisateur peut le fermer.
  • L'utilisateur peut fermer le tableau de bord.

En supposant que les actions ne puissent atteindre le magasin qu'à la suite de l'action de l'utilisateur ou de la réponse du serveur, nous pouvons créer 5 actions.

  • SCOREBOARD_VIEW (suite au clic de l'utilisateur sur le bouton Afficher le tableau de bord)
  • SCOREBOARD_FETCH_SUCCESS (à la suite d'une réponse réussie du serveur)
  • SCOREBOARD_FETCH_FAILURE (suite à une réponse d'erreur du serveur)
  • SCOREBOARD_CLOSE (suite au clic de l'utilisateur sur le bouton de fermeture)
  • MESSAGE_BOX_CLOSE (suite au clic de l'utilisateur sur le bouton de fermeture de la boîte de message)

Ces 5 actions sont suffisantes pour gérer toutes les exigences ci-dessus. Vous pouvez voir que les 4 premières actions n'ont rien à voir avec un quelconque "canard". Chaque action décrit uniquement ce qui s'est passé dans le monde extérieur (l'utilisateur veut faire ceci, le serveur l'a dit) et peut être consommée par n'importe quel réducteur. Nous n'avons pas non plus d'action MESSAGE_BOX_OPEN , car ce n'est pas « ce qui s'est passé » (bien que ce soit ce qui devrait se produire).

La seule façon de changer l'arbre d'état est d'émettre une action, un objet décrivant ce qui s'est passé.README de Redux

Ils sont collés avec ces créateurs d'action :

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

Ensuite, chaque partie du magasin (gouvernée par des réducteurs) peut alors réagir à ces actions :

| Fait partie du magasin/réducteur | Comportement |
| --- | --- |
| tableau de bordVoir | Mettre à jour la visibilité sur true sur SCOREBOARD_VIEW , false sur SCOREBOARD_CLOSE et SCOREBOARD_FETCH_FAILURE |
| tableau de bordChargementIndicateur | Mettre à jour la visibilité sur true sur SCOREBOARD_VIEW , false sur SCOREBOARD_FETCH_* |
| tableau de bordDonnées | Mettre à jour les données dans le magasin sur SCOREBOARD_FETCH_SUCCESS |
| MessageBox | Mettre à jour la visibilité sur true et stocker le message sur SCOREBOARD_FETCH_FAILURE , et mettre à jour la visibilité sur false sur MESSAGE_BOX_CLOSE |

Comme vous pouvez le voir, une seule action peut affecter de nombreuses parties du magasin. Les magasins ne reçoivent qu'une description de haut niveau d'une action (que s'est-il passé ?) plutôt qu'une commande (que faire ?). Par conséquent:

  1. Il est plus facile de repérer les erreurs.

Rien ne peut affecter l'état de la boîte de message. Personne ne peut lui dire de s'ouvrir pour quelque raison que ce soit. Il ne réagit qu'à ce à quoi il est abonné (actions de l'utilisateur et réponses du serveur).

Par exemple, si le serveur ne parvient pas à récupérer le tableau de bord et qu'une boîte de message ne s'affiche pas, vous n'avez pas besoin de savoir pourquoi l'action SHOW_MESSAGE_BOX n'est pas envoyée. Il devient évident que la boîte de message n'a pas géré correctement l'action SCOREBOARD_FETCH_FAILURE .

Un correctif est trivial et peut être rechargé à chaud et parcouru dans le temps.

  1. Les créateurs et réducteurs d'action peuvent être testés séparément.

Vous pouvez tester si les créateurs d'action ont correctement décrit ce qui se passe dans le monde extérieur, sans tenir compte de la façon dont les magasins y réagissent.

De la même manière, les réducteurs peuvent simplement être testés pour savoir s'ils réagissent correctement aux actions du monde extérieur.

(Un test d'intégration serait tout de même très utile.)

Pas de soucis. :) J'apprécie la précision supplémentaire. Il semble en fait que nous soyons d'accord ici. En regardant votre exemple de créateur d'action, viewScoreboard , il ressemble beaucoup à mon exemple de créateur d'action fetchAndProcessThing , juste au-dessus.

Les créateurs et réducteurs d'action peuvent être testés séparément.

Bien que je sois d'accord avec cela, je pense qu'il est souvent plus logique de les tester ensemble. Il est probable que soit votre action _ou_ soit votre réducteur (peut-être les deux) soient super simples et donc la valeur de retour sur effort de tester le simple de manière isolée est plutôt faible. C'est pourquoi j'ai proposé de tester ensemble le créateur d'action, le réducteur et les sélecteurs associés (en tant que "canard").

Mais si une seule "action" utilisateur conceptuelle affecte plusieurs nœuds de l'arbre d'état, vous devrez alors envoyer plusieurs actions.

C'est précisément là que je pense que ce que vous faites diffère de ce qui est considéré comme les meilleures pratiques pour le redux. Je pense que la méthode standard consiste à avoir une action à laquelle plusieurs nœuds de l'arbre d'état répondent.

Ah, observation intéressante @winstonewert. Nous avons suivi un modèle d'utilisation de constantes de type uniques pour chaque paquet "canards" et donc par extension, un réducteur uniquement en réponse aux actions envoyées par ses créateurs d'actions frères. Honnêtement, je ne suis pas sûr de ce que je ressens, au départ, à propos des réducteurs arbitraires répondant à une action. Cela ressemble un peu à une mauvaise encapsulation.

Nous avons suivi un modèle d'utilisation de constantes de type uniques pour chaque paquet "canards"

Notez que nous ne l'approuvons nulle part dans la documentation ;-) Je ne dis pas que c'est mauvais, mais cela donne aux gens certaines idées parfois fausses sur Redux.

donc par extension, un réducteur ne répond qu'aux actions envoyées par ses frères créateurs d'actions

Il n'existe pas de couplage réducteur / créateur d'action dans Redux. C'est purement un truc de Ducks. Certains l'aiment mais cela occulte les atouts fondamentaux du modèle Redux/Flux : les mutations d'état sont découplées les unes des autres et du code qui les provoque.

Honnêtement, je ne suis pas sûr de ce que je ressens, au départ, à propos des réducteurs arbitraires répondant à une action. Cela ressemble un peu à une mauvaise encapsulation.

Selon ce que vous considérez comme des limites d'encapsulation. Les actions sont globales dans l'application, et je pense que c'est bien. Une partie de l'application peut vouloir réagir aux actions d'une autre partie en raison des exigences complexes du produit, et nous pensons que c'est bien. Le couplage est minimal : vous ne dépendez que d'une chaîne et de la forme de l'objet d'action. L'avantage est qu'il est facile d'introduire de nouvelles dérivations des actions dans différentes parties de l'application sans créer des tonnes de câblage avec les créateurs d'action. Vos composants restent ignorants de ce qui se passe exactement lorsqu'une action est envoyée - cela est décidé du côté du réducteur.

Notre recommandation officielle est donc que vous devriez d'abord essayer de faire en sorte que différents réducteurs répondent aux mêmes actions. Si cela devient gênant, alors bien sûr, créez des créateurs d'action séparés. Mais ne commencez pas par cette approche.

Nous recommandons d'utiliser des sélecteurs. En fait, nous recommandons d'exporter les fonctions qui lisent à partir de l'état ("sélecteurs") aux côtés des réducteurs, et de toujours les utiliser dans mapStateToProps au lieu de coder en dur la structure d'état dans le composant. De cette façon, il est facile de modifier la forme de l'état interne. Vous pouvez (mais vous n'êtes pas obligé) utiliser reselect pour les performances, mais vous pouvez également implémenter des sélecteurs naïvement comme dans l'exemple shopping-cart .

Peut-être que cela se résume à savoir si vous programmez en style impératif ou en style réactif. L'utilisation de canards peut entraîner un couplage élevé des actions et des réducteurs, ce qui encourage des actions plus impératives.

  • Dans un style impératif, le magasin reçoit « quoi faire », par exemple SHOW_MESSAGE_BOX ou SHOW_ERROR
  • Dans un style réactif, le magasin reçoit « un fait de ce qui s'est passé », par exemple DATA_FETCHING_FAILED ou USER_ENTERED_INVALID_THING_ID . Le magasin réagit en conséquence.

Dans l'exemple précédent, je n'ai pas l'action SHOW_MESSAGE_BOX ou le créateur d'action showError('Invalid thing id="'+id+'"') , car ce n'est pas un fait. C'est une commande.

Une fois que ce fait entre dans le magasin, vous pouvez traduire ce fait en commandes, à l'intérieur de vos réducteurs purs, par exemple

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

Lorsqu'une action entre dans le magasin comme « un fait » plutôt que « une commande », il y a moins de chance qu'elle se passe mal, parce que, eh bien, c'est un fait.

Maintenant, si vos réducteurs ont mal interprété ce fait, il peut être corrigé facilement et le correctif peut voyager dans le temps. Si vos créateurs d'action interprètent mal ce fait, cependant, vous devez réexécuter vos créateurs d'action.

Vous pouvez également modifier votre réducteur de sorte que lorsque USER_ENTERED_INVALID_THING_ID déclenche, le champ de texte de l'ID de l'objet soit réinitialisé. Et ce changement voyage aussi dans le temps. Vous pouvez également localiser votre message d'erreur et sans rafraîchir la page. Cela resserre la boucle de rétroaction et facilite grandement le débogage et les ajustements.

(Je ne parle que des avantages ici, bien sûr, il y a des inconvénients. Vous devez réfléchir beaucoup plus à la façon de représenter ce fait, étant donné que votre magasin ne peut répondre à ces faits que de manière synchrone et sans effets secondaires. Voir la discussion sur modèles alternatifs async/effets secondaires et cette question que j'ai postée sur StackOverflow . Je suppose que nous n'avons pas encore


Honnêtement, je ne suis pas sûr de ce que je ressens, au départ, à propos des réducteurs arbitraires répondant à une action. Cela ressemble un peu à une mauvaise encapsulation.

Il est également très courant que plusieurs composants prennent des données du même magasin. Il est également assez courant qu'un seul composant dépende des données de plusieurs parties du magasin. Cela ne ressemble-t-il pas aussi un peu à une mauvaise encapsulation ? Pour devenir vraiment modulaire, un composant React ne devrait-il pas également être à l'intérieur du bundle « canard » ? (L' architecture Elm fait cela .)

React rend votre interface utilisateur réactive (d'où son nom) en traitant les données de votre boutique comme un fait. Vous n'avez donc pas à indiquer à votre point de vue « comment mettre à jour l'interface utilisateur ».

De la même manière, je pense également que Redux/Flux rend votre modèle de données réactif , en traitant les actions comme un fait, vous n'avez donc pas à dire à votre modèle de données comment se mettre à jour.

Merci d'avoir pris le temps d'écrire et de partager vos réflexions, @dtinth. Merci également à @gaearon d' avoir participé à cette discussion. (Je sais que vous avez une tonne de choses en cours.) Vous m'avez tous les deux donné des choses supplémentaires à considérer. :)

Il est également très courant que plusieurs composants prennent des données du même magasin. Il est également assez courant qu'un seul composant dépende des données de plusieurs parties du magasin. Cela ne ressemble-t-il pas aussi un peu à une mauvaise encapsulation ?

Euh... une partie de cela est subjective, mais non. Je pense aux créateurs d'action et aux sélecteurs exportés comme à l'API du module.

Quoi qu'il en soit, je pense que cela a été une bonne discussion. Comme Thai l'a mentionné dans sa réponse précédente, il y a des avantages et des inconvénients à ces approches dont nous discutons. Cela a été agréable d'avoir un aperçu des autres approches. :)

Soit dit en passant, la boîte de message est un bon exemple de l'endroit où je préférerais avoir un créateur d'action séparé pour l'affichage. Principalement parce que je veux passer un moment où il a été créé afin qu'il puisse être rejeté automatiquement (et le créateur d'action est l'endroit où vous appelez l'impure Date.now() ), parce que je veux configurer une minuterie pour le rejeter, je vouloir éviter ce minuteur, etc. Je considérerais donc une boîte de message comme le cas où son "flux d'action" est suffisamment important pour justifier ses actions personnelles. Cela dit, peut-être que ce que j'ai décrit peut être résolu plus élégamment par https://github.com/yelouafi/redux-saga.

J'ai d'abord écrit ceci dans le chat Discord Reactiflux, mais on m'a demandé de le coller ici.

J'ai beaucoup pensé aux mêmes choses récemment. J'ai l'impression que les mises à jour d'état sont divisées en trois parties.

  1. Le créateur de l'action reçoit le minimum d'informations nécessaires pour exécuter la mise à jour. C'est-à-dire que tout ce qui peut être calculé à partir de l'état actuel ne doit pas s'y trouver.
  2. L'état est interrogé pour toute information dont vous avez besoin pour effectuer les mises à jour (par exemple, lorsque vous souhaitez copier Todo avec l'id X, vous récupérez les attributs de Todo avec l'id X afin que vous puissiez faire une copie). Cela peut être fait dans le créateur d'action, et cette information est ensuite incluse dans l'objet d'action. Cela se traduit par des objets d'action de graisse . OU il pourrait être calculé dans le réducteur - objets à action mince .
  3. Sur la base de ces informations, une logique de réduction pure est appliquée pour obtenir l'état suivant.

Maintenant, le problème est de savoir quoi mettre dans le créateur d'action et quoi dans le réducteur, le choix entre les objets d'action gras et minces. Si vous mettez toute la logique dans le créateur d'action, vous vous retrouvez avec de gros objets d'action qui déclarent essentiellement les mises à jour de l'état. Les réducteurs deviennent purs, idiots, rajoutez ceci, supprimez cela, mettez à jour ces fonctions. Ils seront faciles à composer. Mais pas beaucoup de votre logique métier sera là.

Si vous mettez plus de logique dans le réducteur, vous vous retrouvez avec de beaux objets d'action légers, la plupart de votre logique de données en un seul endroit, mais vos réducteurs sont plus difficiles à composer car vous pourriez avoir besoin d'informations provenant d'autres branches. Vous vous retrouvez avec de gros réducteurs ou des réducteurs qui prennent des arguments supplémentaires de plus haut dans l'état.

Je ne sais pas quelle est la réponse à ces problèmes, je ne sais pas s'il y en a encore une

Merci d'avoir partagé ces pensées @tommikaikkonen. Je suis encore moi-même indécis sur la "réponse" ici. Je suis d'accord avec ton résumé. J'ajouterais une petite note à la section "mettre toute la logique dans le créateur d'action...", c'est-à-dire qu'elle vous permet d'utiliser des sélecteurs (partagés) pour lire des données, ce qui peut être agréable dans certains cas.

Ceci est une discussion intéressante! Déterminer où placer le code dans une application redux est un problème, je suppose que nous sommes tous confrontés. J'aime l'idée du CQRS d'enregistrer simplement ce qui s'est passé.
Mais je peux voir un décalage d'idées ici parce que dans CQRS, AFAIK, la meilleure pratique consiste à créer un état dénormalisé à partir de l'action/des événements qui est directement consommable par les vues. Mais en redux, la meilleure pratique consiste à créer un état entièrement normalisé et à extraire les données de vos vues via des sélecteurs.
Si nous construisons un état dénormalisé qui est directement consommable par la vue, alors je pense que le problème selon lequel un réducteur veut des données dans un autre réducteur disparaît (car chaque réducteur peut simplement stocker toutes les données dont il a besoin sans se soucier de la normalisation). Mais ensuite, nous rencontrons d'autres problèmes lors de la mise à jour des données. C'est peut-être le cœur de la discussion ?

Issu de nombreuses années de développement orienté objet. Redux se sent comme un grand pas en arrière. Je me retrouve à implorer de créer des classes qui encapsulent des événements (créateurs d'actions) et la logique métier. J'essaie toujours de trouver un compromis significatif, mais je n'ai pas encore réussi à le faire. Est-ce que quelqu'un d'autre ressent la même chose ?

La programmation orientée objet encourage l'association des lectures et des écritures. Cela rend un tas de choses problématiques : snapshots et rollback, journalisation centralisée, débogage des mutations d'état erronées, mises à jour efficaces et granulaires. Si vous ne pensez pas que ce sont vos problèmes, si vous savez comment les éviter lors de l'écriture de code MVC orienté objet traditionnel, et si Redux introduit plus de problèmes qu'il n'en résout dans votre application, n'utilisez pas Redux :wink: .

@jayesbe Issu d'une formation en programmation orientée objet, vous constaterez peut-être que cela entre en conflit avec les idées émergentes en programmation. Ceci est l'un des nombreux articles sur le sujet : https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptionally-bad/.

En séparant les actions de la transformation des données, les tests d'application des règles métier sont plus simples. Les transformations deviennent moins dépendantes du contexte de l'application.

Cela ne signifie pas abandonner des objets ou des classes en Javascript. Par exemple, les composants React sont implémentés sous forme d'objets, et maintenant de classes. Mais les composants React sont conçus simplement pour créer une projection des données fournies. Nous vous encourageons à utiliser des composants purs qui ne stockent pas d'état.

C'est là qu'intervient Redux : organiser l'état de l'application et rassembler les actions et la transformation correspondante de l'état de l'application.

@johnsoftek merci pour le lien. Cependant, sur la base de mon expérience au cours des 10 dernières années.. Je ne suis pas d'accord avec cela, mais nous n'avons pas besoin d'entrer dans le débat entre OO et non-OO ici. Le problème que j'ai est d'organiser le code et l'abstraction.

Mon objectif est de créer une seule application/une seule architecture qui puisse (en utilisant uniquement les valeurs de configuration) être utilisée pour créer des centaines d'applications. Le cas d'utilisation auquel je dois faire face est la gestion d'une solution logicielle en marque blanche utilisée par de nombreux clients.

J'ai trouvé ce que je pense être un compromis intéressant.. et je pense qu'il le gère assez bien, mais il peut ne pas répondre aux normes des foules de programmation fonctionnelle. J'aimerais quand même le mettre là-bas.

J'ai une seule classe d'application qui est autonome avec toute la logique métier, les wrappers d'API, etc. dont j'ai besoin pour s'interfacer avec mon application côté serveur.

Exemple..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

J'ai créé une seule instance de cet objet et l'ai placé dans le contexte.. en étendant le fournisseur Redux

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

Ensuite, j'ai utilisé ce fournisseur en tant que tel

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

enfin dans mon composant j'ai utilisé mon application ..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

L'Action Creator n'est donc qu'une fonction de rappel à ma logique métier.

                      app.login(value, actions.login);

Cette solution semble bien fonctionner pour le moment même si je n'ai commencé qu'avec l'authentification.

Je pense que je pourrais également passer le magasin dans mon instance d'application, mais je ne veux pas le faire car je ne veux pas que le magasin subisse une mutation fortuite. Bien que l'accès au magasin puisse être utile. J'y réfléchirai plus si j'en ai besoin.

J'ai trouvé ce que je pense être un compromis intéressant.. et je pense qu'il le gère assez bien, mais il peut ne pas répondre aux normes des foules de programmation fonctionnelle.

Vous ne trouverez pas « la foule fonctionnelle » ici :wink: . La raison pour laquelle nous choisissons des solutions fonctionnelles dans Redux n'est pas parce que nous sommes dogmatiques, mais parce qu'elles résolvent certains problèmes que les gens font souvent à cause des classes. Par exemple, séparer les réducteurs des créateurs d'actions nous permet de séparer les lectures et les écritures, ce qui est important pour la journalisation et la reproduction des bogues. Les actions étant des objets simples rendent l'enregistrement et la relecture possibles car elles sont sérialisables. De même, l'état étant un objet simple plutôt qu'une instance de MyAppState il est très facile de le sérialiser sur le serveur et de le désérialiser sur le client pour le rendu du serveur, ou de conserver des parties de celui-ci dans localStorage . L'expression des réducteurs en tant que fonctions nous permet de mettre en œuvre un voyage dans le temps et un rechargement à chaud, et l'expression des sélecteurs en tant que fonctions facilite l'ajout de la mémorisation. Tous ces avantages n'ont rien à voir avec le fait que nous sommes une « foule fonctionnelle » et tout à voir avec la résolution de tâches spécifiques pour lesquelles cette bibliothèque a été créée.

J'ai créé une seule instance de cet objet et l'ai placé dans le contexte.. en étendant le fournisseur Redux

Cela me semble tout à fait sensé. Nous n'avons pas une haine irrationnelle des classes. Le fait est que nous préférons ne pas les utiliser dans les cas où ils sont fortement limitatifs (comme pour les réducteurs ou les objets d'action), mais c'est bien de les utiliser pour générer des objets d'action, par exemple.

J'éviterais cependant d'étendre Provider car c'est fragile. Cela n'est pas nécessaire : React fusionne le contexte des composants, vous pouvez donc simplement l'envelopper à la place.

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

Il se lit en fait plus facilement à mon avis et est moins fragile.

Donc, dans l'ensemble, votre approche est tout à fait logique. L'utilisation d'une classe dans ce cas n'est pas vraiment différente de quelque chose comme createActions(config) qui est un modèle que nous recommandons également si vous devez paramétrer les créateurs d'action. Il n'y a absolument rien de mal à cela.

Nous vous déconseillons uniquement d'utiliser des instances de classe pour les objets d'état et d'action, car les instances de classe rendent la sérialisation très délicate. Pour les réducteurs, nous ne recommandons pas non plus d'utiliser des classes car il sera plus difficile d'utiliser une composition de réducteur, c'est-à-dire des réducteurs qui appellent d'autres réducteurs. Pour tout le reste, vous pouvez utiliser n'importe quel moyen d'organisation du code, y compris les classes.

Si votre application et votre configuration sont immuables (et je pense qu'elles devraient l'être, mais j'ai peut-être bu trop de cool-aid fonctionnel), alors vous pouvez envisager l'approche suivante :

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

Et puis dans mapStateToProps :

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

Ensuite, vous n'avez pas besoin de la technique de fournisseur que vous avez adoptée, vous obtenez simplement l'objet d'application de l'état. Grâce à resélectionner, l'objet Application ne sera construit que lorsque la configuration change, ce qui n'est probablement qu'une fois.

Là où je pense que cette approche peut avoir un avantage, c'est qu'elle vous permet facilement d'étendre l'idée d'avoir plusieurs de ces objets et de faire également dépendre ces objets d'autres parties de l'État. Par exemple, vous pourriez avoir une classe UserControl avec des méthodes login/logout/etc qui a accès à la fois à la configuration et à une partie de votre état.

Donc, dans l'ensemble, votre approche est tout à fait logique.

:+1: Merci ça aide. Je suis d'accord avec l'amélioration de MyProvider. Je vais mettre à jour mon code pour suivre. L'un des plus gros problèmes que j'ai eu lors de l'apprentissage de Redux pour la première fois était la notion sémantique de "Créateurs d'action". Pour moi, c'était une sorte de prise de conscience qui était comme… ce sont des événements qui sont expédiés.

@winstonewert est-ce que

L'un des plus gros problèmes que j'ai eu lors de l'apprentissage de Redux pour la première fois était la notion sémantique de "Créateurs d'action". Pour moi, c'était une sorte de prise de conscience qui était comme… ce sont des événements qui sont expédiés.

Je dirais qu'il n'y a pas du tout de notion sémantique de créateurs d'action dans Redux. Il existe une notion sémantique d'Actions (qui décrivent ce qui s'est passé et sont à peu près équivalentes aux événements mais pas tout à fait—par exemple, voir la discussion au n° 351). Les créateurs d'action ne sont qu'un modèle pour organiser le code. Il est pratique d'avoir des usines pour les actions car vous voulez vous assurer que les actions du même type ont une structure cohérente, ont le même effet secondaire avant d'être envoyées, etc. Mais du point de vue de Redux, les créateurs d'action n'existent pas—Redux ne voit que les actions.

est-ce que createSelector est disponible sur react-native ?

Il est disponible dans Reselect qui est du JavaScript simple sans dépendances et peut fonctionner sur le web, sur le natif, sur le serveur, etc.

Ahhh. ok j'ai compris. Les choses sont beaucoup plus claires. À votre santé.

Ne pas imbriquer d'objets dans mapStateToProps et mapDispatchToProps

J'ai récemment rencontré un problème de perte d'objets imbriqués lorsque Redux fusionne mapStateToProps et mapDispatchToProps (voir reactjs/react-redux#324). Bien que @gaearon fournisse une solution qui me permette d'utiliser des objets imbriqués, il poursuit en disant qu'il s'agit d'un anti-modèle :

Notez que le regroupement d'objets comme celui-ci entraînera des allocations inutiles et rendra également les optimisations de performances plus difficiles, car nous ne pouvons plus compter sur une égalité superficielle des accessoires de résultat pour savoir s'ils ont changé. Vous verrez donc plus de rendus qu'avec une approche simple sans espacement de noms que nous recommandons dans la doc.

@bvaughn a dit

les réducteurs devraient être stupides et simples

et nous devrions mettre la plupart de la logique commerciale dans les créateurs d'action, je suis tout à fait d'accord avec cela. Mais si tout est passé à l'action, pourquoi devons-nous encore créer manuellement des fichiers et des fonctions de réduction ? Pourquoi ne pas stocker directement les données qui ont été exploitées dans les actions ?

Cela m'a troublé pendant un certain temps...

pourquoi devons-nous toujours créer des fichiers et des fonctions de réduction manuellement ?

Comme les réducteurs sont des fonctions pures, s'il y a une erreur dans la logique de mise à jour d'état, vous pouvez recharger à chaud le réducteur. L'outil de développement peut ensuite rembobiner l'application à son état initial, puis rejouer toutes les actions, à l'aide du nouveau réducteur. Cela signifie que vous pouvez corriger les bogues de mise à jour de l'état sans avoir à revenir en arrière et à réexécuter manuellement l'action. C'est l'avantage de conserver la majorité de la logique de mise à jour d'état dans le réducteur.

C'est l'avantage de conserver la majorité de la logique de mise à jour d'état dans le réducteur.

@dtinth Juste pour clarifier, en disant "logique de mise à jour d'état", voulez-vous dire "logique métier"?

Je ne suis pas sûr que mettre la majeure partie de votre logique dans les créateurs d'action soit une bonne idée. Si tous vos réducteurs ne sont que des fonctions triviales acceptant des actions de type ADD_X , alors il est peu probable qu'ils contiennent des erreurs - super ! Mais ensuite, toutes vos erreurs ont été transmises à vos créateurs d'action et vous perdez la grande expérience de débogage à laquelle @dtinth fait allusion.

Mais aussi comme @tommikaikkonen mentionné, ce n'est pas si simple d'écrire des réducteurs complexes. Mon intuition est que c'est là que vous voudriez pousser si vous vouliez profiter des avantages de Redux - sinon, au lieu de pousser les effets secondaires à l'extrême, vous poussez vos fonctions pures pour gérer uniquement les tâches les plus triviales, laissant la plupart de votre application dans un enfer étatique. :)

@sompylasar "logique métier" et "logique de mise à jour d'état" sont, imo, un peu la même chose.

Cependant, pour répondre à mes propres spécificités de mise en œuvre, mes actions sont principalement des recherches sur les entrées dans l'action. En fait, toutes mes actions sont pures car j'ai déplacé toute ma "logique métier" dans un contexte d'application.

à titre d'exemple.. c'est mon réducteur typique

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

Mes actions types :

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

Ma logique métier est définie dans un objet stocké dans le contexte, c'est à dire..

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

Si vous voyez mon commentaire ci-dessus, je me débattais également avec la meilleure approche. J'ai finalement remanié et réglé en passant le magasin dans le constructeur de mon objet d'application et en connectant toutes les actions au répartiteur à ce point central. Toutes les actions que mon application connaît sont affectées ici.

Je n'utilise plus mapDispatchToProps() nulle part. Pour Redux, je mapStateToProps uniquement lors de la création d'un composant connecté. Si j'ai besoin de déclencher des actions, je peux les déclencher via mon objet d'application via le contexte.

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

Le composant ci-dessus n'a pas besoin d'être redux connecté. Il peut toujours envoyer des actions. Bien sûr, si vous devez mettre à jour l'état dans le composant, vous le transformerez en un composant connecté pour vous assurer que le changement d'état est propagé.

C'est ainsi que j'ai organisé mon cœur de « logique métier ». Étant donné que mon état est vraiment maintenu sur un serveur principal, cela fonctionne très bien pour mon cas d'utilisation.

L'endroit où vous stockez votre « logique métier » dépend vraiment de vous et de la manière dont il s'adapte à votre cas d'utilisation.

@jayesbe La partie suivante signifie que vous n'avez pas de "logique métier" dans les réducteurs et, de plus, la structure d'état s'est déplacée dans les créateurs d'action qui créent la charge utile qui est transférée dans le magasin via le réducteur :

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

@jayesbe Mes actions et mes réducteurs sont très similaires aux vôtres, certaines actions reçoivent un objet de réponse réseau simple comme argument, et j'ai encapsulé la logique comment gérer les données de réponse dans l'action et enfin renvoyer un objet très simple comme valeur renvoyée puis passer à réducteur via appel dispatch(). Tout comme ce que tu as fait. Le problème est que si votre action écrite de cette manière, votre action a presque tout fait et que la responsabilité de votre réducteur sera très légère, pourquoi devons-nous transférer les données à stocker manuellement si le réducteur se contente de diffuser simplement l'objet action ? Redux le doser automatiquement pour nous n'est pas du tout une chose difficile.

Pas nécessairement. Mais la plupart du temps, une partie du processus commercial
implique la mise à jour de l'état de l'application selon les règles métier,
vous devrez donc peut-être y mettre une logique métier.

Pour un cas extrême, regardez ceci :
« Moteurs RTS synchrones et histoire de désynchronisations » @ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

Le 5 avril 2016 à 17h54, "John Babak" [email protected] a écrit :

C'est l'avantage de garder la majorité de la logique de mise à jour de l'état dans le
réducteur.

@dtinth https://github.com/dtinth Juste pour clarifier, en disant
« logique de mise à jour de l'état » voulez-vous dire « logique métier » ?

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

@LumiaSaki Le conseil de garder vos réducteurs simples tout en gardant une logique complexe dans les créateurs d'action va à l'encontre de la manière recommandée d'utiliser Redux. Le modèle recommandé par Redux est le contraire : gardez les créateurs d'action simples tout en conservant une logique complexe dans les réducteurs. Bien sûr, vous êtes de toute façon libre de mettre toute votre logique dans les créateurs d'action, mais ce faisant, vous ne suivez pas le paradigme Redux, même si vous utilisez Redux.

Pour cette raison, Redux ne transférera pas automatiquement les données des actions vers le magasin. Parce que ce n'est pas la façon dont vous êtes censé utiliser Redux. Redux ne sera pas modifié pour faciliter son utilisation d'une manière autre que celle prévue. Bien sûr, vous êtes absolument libre de faire ce qui fonctionne pour vous, mais ne vous attendez pas à ce que Redux change pour cela.

Pour ce que ça vaut, je produis mes réducteurs en utilisant quelque chose comme :

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

defineActions renvoie à la fois le réducteur et les créateurs d'action. De cette façon, je trouve très facile de garder ma logique de mise à jour à l'intérieur du réducteur sans avoir à passer beaucoup de temps à écrire des créateurs d'action triviaux.

Si vous insistez pour garder votre logique dans vos créateurs d'action, vous ne devriez avoir aucun problème à automatiser les données vous-même. Votre réducteur est une fonction, et il peut faire ce qu'il veut. Ainsi, votre réducteur pourrait être aussi simple que :

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

En développant les points précédents concernant la logique métier, je pense que vous pouvez séparer votre logique métier en deux parties :

  • La partie non déterministe. Cette partie utilise des services externes, du code asynchrone, l'heure système ou un générateur de nombres aléatoires. À mon avis, cette partie est mieux gérée par une fonction d'E/S (ou un créateur d'action). Je les appelle le processus d'affaires.

Considérez un logiciel de type IDE où l'utilisateur peut cliquer sur le bouton Exécuter, et il compilerait et exécuterait l'application. (Ici, j'utilise une fonction asynchrone qui prend un magasin, mais vous pouvez utiliser redux-thunk place.)

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

  • La partie déterministe. Cette partie a un résultat totalement prévisible. Étant donné le même état et le même événement, le résultat est toujours prévisible. À mon avis, cette partie est mieux gérée par un réducteur. C'est ce que j'appelle les règles métier.

``` js
t'importe de 'updeep'

export const reducer = createReducer({
// [nom de l'action] : action => currentState => nextState
startCompiling : () => u({ compilation : true }),
errorCompiling : ({ error }) => u({ compilation : false, compileError : error }),
startRunning : ({ app }) => u({
en cours d'exécution : () => application,
compilation : faux
}),
stopRunning : () => u({ running : false }),
jeterErreurCompile : () => u({ compileError : null }),
// ...
})
```

J'essaie de mettre autant de code que possible dans ce terrain déterministe, tout en gardant à l'esprit que la seule responsabilité du réducteur est de maintenir l'état de l'application cohérent, compte tenu des actions entrantes. Rien de plus. En dehors de cela, je le ferais en dehors de Redux, car Redux n'est qu'un conteneur d'état.

@dtinth Super, car l'exemple précédent dans https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 est totalement différent de ce que vous avez écrit dans https://github.com/reactjs/redux/ issues/1171#issuecomment -205888533 -- il suggère de construire un morceau d'état dans les créateurs d'action et de les passer dans les réducteurs pour qu'ils diffusent simplement les mises à jour (cette approche me semble erronée, et je suis d'accord avec la même chose soulignée dans https://github.com/reactjs/redux/issues/1171#issuecomment-205865840).

@winstonewert

Le modèle recommandé par Redux est le contraire : gardez les créateurs d'action simples tout en conservant une logique complexe dans les réducteurs.

Comment pouvez-vous mettre une logique complexe dans les rudducteurs et les garder toujours purs ?

Si j'appelle fetch() par exemple et que je charge des données à partir du serveur, puis les traite d'une manière ou d'une autre. Je n'ai pas encore vu d'exemple de réducteur qui a une "logique complexe"

@jayesbe : Euh... "complexe" et "pur" sont orthogonaux. Vous pouvez avoir une logique conditionnelle ou une manipulation _vraiment_ compliquée à l'intérieur d'un réducteur, et tant que ce n'est qu'une fonction de ses entrées sans effets secondaires, c'est toujours pur.

Si vous avez un état local complexe (pensez à un éditeur de publication, une arborescence, etc.) ou si vous gérez des choses comme des mises à jour optimistes, vos réducteurs contiendront une logique complexe. Cela dépend vraiment de l'application. Certains ont des demandes complexes, d'autres ont des mises à jour d'état complexes. Certains ont les deux :-)

@markerikson ok les instructions logiques sont une chose... mais exécuter des tâches spécifiques ? Comme disons que j'ai une action qui dans un cas déclenche trois autres actions, ou dans un autre cas déclenche deux actions distinctes et séparées. Cette logique + exécution des tâches ne semble pas devoir aller dans des réducteurs.

Mes données d'état / état du modèle sont sur le serveur, l'état de la vue est distinct du modèle de données mais la gestion de cet état est sur le client. L'état de mon modèle de données est simplement transmis à la vue. C'est ce qui rend mes réducteurs et mes actions si légers.

@jayesbe : Je ne pense pas que quiconque ait jamais dit que le déclenchement d'autres actions devrait aller dans un réducteur. Et en fait, il ne devrait pas. Le travail d'un réducteur est simplement (currentState + action) -> newState .

Si vous devez lier plusieurs actions, soit vous le faites dans quelque chose comme un thunk ou une saga et vous les lancez dans l'ordre, soit vous avez quelque chose qui écoute les changements d'état, ou vous utilisez un middleware pour intercepter une action et effectuer un travail supplémentaire.

Je suis un peu confus sur ce qu'est la discussion à ce stade, pour être honnête.

@markerikson, le sujet semble concerner la "logique métier" et où elle va. Le fait est que tout dépend de l'application. Il y a différentes manières de s'y prendre. Certains plus complexes que d'autres. Si vous trouvez un bon moyen de résoudre votre problème, cela vous facilite la maintenance et l'organisation. C'est tout ce qui compte vraiment. Mon implémentation est très légère pour mon cas d'utilisation même si elle va à l'encontre du paradigme.

Comme vous le remarquez, tout ce qui compte vraiment, c'est ce qui fonctionne pour vous. Mais voici comment j'aborderais les problèmes que vous avez posés.

Si j'appelle fetch() par exemple et que je charge des données à partir du serveur, puis les traite d'une manière ou d'une autre. Je n'ai pas encore vu d'exemple de réducteur qui a une "logique complexe"

Mon réducteur prend une réponse brute de mon serveur et met à jour mon état avec. De cette façon, le traitement de la réponse dont vous parlez se fait dans mon réducteur. Par exemple, la demande peut être de récupérer des enregistrements JSON pour mon serveur, que le réducteur colle dans mon cache d'enregistrements local.

k les instructions logiques sont une chose... mais exécuter des tâches spécifiques ? Comme disons que j'ai une action qui dans un cas déclenche trois autres actions, ou dans un autre cas déclenche deux actions distinctes et séparées. Cette logique + exécution des tâches ne semble pas devoir aller dans des réducteurs.

Cela dépend de ce que vous faites. Évidemment, dans le cas de l'extraction du serveur, une action en déclenchera une autre. C'est entièrement dans la procédure Redux recommandée. Cependant, vous pourriez également faire quelque chose comme ceci :

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

Ce n'est pas la manière recommandée d'utiliser Redux. La méthode Redux recommandée est d'avoir une seule action CREATE_FOOBAR qui provoque tous ces changements souhaités.

@winstonewert :

Ce n'est pas la manière recommandée d'utiliser Redux. La méthode Redux recommandée est d'avoir une seule action CREATE_FOOBAR qui provoque tous ces changements souhaités.

Vous avez un pointeur vers un endroit qui est spécifié ? Parce que lorsque je faisais des recherches pour la page FAQ, ce que j'ai trouvé était "ça dépend", directement de Dan. Voir http://redux.js.org/docs/FAQ.html#actions -multiple-actions et cette réponse de Dan sur SO .

« Logique métier » est vraiment un terme assez large. Il peut couvrir des choses comme « Est-ce qu'il s'est passé quelque chose ? », « Que faisons-nous maintenant que cela s'est produit ? », « Est-ce valide ? », et ainsi de suite. Sur la base de la conception de Redux, ces questions _peuvent_ être répondues à divers endroits en fonction de la situation, bien que je considère « est-ce arrivé » comme étant davantage une responsabilité de créateur d'action, et « et maintenant » est presque certainement une responsabilité de réducteur.

Dans l'ensemble, mon point de vue sur cette _toute question_ de « logique d'entreprise » est : _ « ça dépend _ ». Il y a des raisons pour lesquelles vous pourriez vouloir faire une analyse de requête dans un créateur d'action, et des raisons pour lesquelles vous pourriez vouloir le faire dans un réducteur. Il y a des moments où votre réducteur peut simplement "prendre cet objet et le mettre dans mon état", et d'autres fois où votre réducteur peut être une logique conditionnelle très complexe. Il y a des moments où votre créateur d'action peut être très simple, et d'autres moments où cela peut être complexe. Il y a des moments où il est logique d'envoyer plusieurs actions d'affilée pour représenter les étapes d'un processus, et d'autres fois où vous voudriez seulement envoyer une action générique "THING_HAPPENED" pour tout représenter.

La seule règle stricte avec laquelle je serais d'accord est "le non-déterminisme dans les créateurs d'action, le déterminisme pur dans les réducteurs". C'est une donnée.

Autre que ça? Trouvez quelque chose qui fonctionne pour vous. Être cohérent. Sachez pourquoi vous le faites d'une certaine manière. Allez avec.

même si je verrais "est-ce que c'est arrivé" comme une responsabilité de créateur d'action, et "quoi maintenant" est presque certainement une responsabilité de réducteur.

C'est pourquoi il y a une discussion parallèle sur la façon de mettre les effets secondaires, c'est-à-dire la partie non pure du « quoi maintenant », dans les réducteurs : #1528 et d'en faire de pures descriptions de ce qui devrait arriver, comme les prochaines actions à envoyer.

Le modèle que j'ai utilisé est :

  • Que faire : Action/Créateur d'action
  • Comment faire : Middleware (par exemple, middleware qui écoute les actions « async » et appelle mon objet API.)
  • Que faire du résultat : Réducteur

Plus tôt dans ce fil, la déclaration de Dan était :

Notre recommandation officielle est donc que vous devriez d'abord essayer de faire en sorte que différents réducteurs répondent aux mêmes actions. Si cela devient gênant, alors bien sûr, créez des créateurs d'action séparés. Mais ne commencez pas par cette approche.

De là, je suppose que l'approche recommandée est d'envoyer une action par événement. Mais, pragmatiquement, faites ce qui fonctionne.

@winstonewert : Dan fait référence au modèle "composition du réducteur", c'est-à-dire "est une action qui n'est jamais écoutée que par un seul réducteur" vs "de nombreux réducteurs peuvent répondre à la même action". Dan est très attaché aux réducteurs arbitraires répondant à une seule action. D'autres préfèrent des trucs comme l'approche « canards », où les réducteurs et les actions sont TRÈS étroitement regroupés, et un seul réducteur gère une action donnée. Donc, cet exemple ne concerne pas "l'envoi de plusieurs actions en séquence", mais plutôt "combien de parties de ma structure de réducteur s'attendent à répondre à cela".

Mais, pragmatiquement, faites ce qui fonctionne.

:+1:

@sompylasar Je vois l'erreur de mes manières en ayant la structure d'état dans mes actions. Je peux facilement déplacer la structure d'état dans mes réducteurs et simplifier mes actions. À votre santé.

Il me semble que c'est la même chose.

Soit vous avez une seule action déclenchant plusieurs réducteurs provoquant plusieurs changements d'état, soit vous avez plusieurs actions déclenchant chacune un seul réducteur provoquant un seul changement d'état. Le fait que plusieurs réducteurs répondent à une action et qu'un événement envoie plusieurs actions sont des solutions alternatives au même problème.

Dans la question StackOverflow que vous mentionnez, il déclare :

Gardez le journal des actions aussi près que possible de l'historique des interactions des utilisateurs. Cependant, si cela rend les réducteurs difficiles à mettre en œuvre, envisagez de diviser certaines actions en plusieurs, si une mise à jour de l'interface utilisateur peut être considérée comme deux opérations distinctes qui se trouvent être ensemble.

À mon avis, Dan soutient le maintien d'une action par interaction utilisateur comme le moyen idéal. Mais il est pragmatique, quand cela rend le réducteur délicat à mettre en œuvre, il approuve le fractionnement de l'action.

Je visualise ici quelques cas d'utilisation similaires mais quelque peu différents :

1) Une action nécessite des mises à jour de plusieurs zones de votre état, en particulier si vous utilisez combineReducers pour avoir des fonctions de

  • faire en sorte que le réducteur A et le réducteur B répondent à la même action et mettent à jour leurs bits d'état indépendamment
  • faites en sorte que votre thunk insère TOUTES les données pertinentes dans l'action afin que tout réducteur puisse accéder à d'autres bits d'état en dehors de son propre morceau
  • ajouter un autre réducteur de niveau supérieur qui récupère des bits de l'état du réducteur A et le remettre dans un cas spécial au réducteur B ?
  • envoyer une action destinée au réducteur A avec les bits d'état dont il a besoin, et une seconde action au réducteur B avec ce dont il a besoin ?

2) Vous avez un ensemble d'étapes qui se déroulent dans une séquence spécifique, chaque étape nécessitant une mise à jour de l'état ou une action middleware. Est-ce que tu:

  • Répartir chaque étape individuelle en tant qu'action distincte pour représenter les progrès et laisser les réducteurs individuels répondre à des actions spécifiques ?
  • Envoyer un grand événement et faire confiance aux réducteurs pour le gérer de manière appropriée ?

Alors oui, certainement un certain chevauchement, mais je pense qu'une partie de la différence d'image mentale ici réside dans les différents cas d'utilisation.

@markerikson Donc, votre conseil est « cela dépend de la situation que vous rencontrez », et comment équilibrer la « logique commerciale » sur les actions ou les réducteurs dépend de votre considération, nous devrions également profiter autant que possible des avantages de la fonction pure ?

Oui. Les réducteurs _doivent_ être purs, comme une exigence Redux (sauf dans 0,00001 % des cas particuliers). Les créateurs d'action n'ont absolument

Et oui, de mon point de vue, c'est à vous en tant que développeur de déterminer quel est l'équilibre approprié pour la logique de votre propre application et où elle réside. Il n'y a pas de règle absolue pour savoir de quel côté de la division créateur/réducteur d'action il doit vivre. (Euh, à l'exception de la chose "déterminisme / non-déterminisme" que j'ai mentionnée ci-dessus. Ce que je voulais clairement faire référence dans ce commentaire. Évidemment.)

@cpsubrian

Que faire du résultat : réducteur

En fait, c'est pourquoi les sagas sont faites pour : gérer des effets comme "si cela arrivait, alors cela devrait aussi arriver"


@markerikson @LumiaSaki

Les créateurs d'action n'ont absolument pas besoin d'être purs, et c'est en fait là que la plupart de vos « impuretés » vivront.

En fait, les créateurs d'action ne sont même pas tenus d'être impurs ou même d'exister.
Voir http://stackoverflow.com/a/34623840/82609

Et oui, de mon point de vue, c'est à vous en tant que développeur de déterminer quel est l'équilibre approprié pour la logique de votre propre application et où elle réside. Il n'y a pas de règle absolue pour savoir de quel côté de la division créateur/réducteur d'action il doit vivre.

Oui mais ce n'est pas si évident de remarquer les inconvénients de chaque approche sans expérience :) Voir aussi mon commentaire ici : https://github.com/reactjs/redux/issues/1171#issuecomment -167585575

Aucune règle stricte ne fonctionne correctement pour la plupart des applications simples, mais si vous souhaitez créer des composants réutilisables, ces composants ne doivent pas être conscients de quelque chose en dehors de leur propre portée.

Ainsi, au lieu de définir une liste d'actions globale pour l'ensemble de votre application, vous pouvez commencer à diviser votre application en composants réutilisables et chaque composant a sa propre liste d'actions et ne peut que les répartir/réduire. Le problème est alors, comment exprimez-vous "quand la date est sélectionnée dans mon sélecteur de date, alors nous devrions enregistrer un rappel sur cet élément à faire, afficher un toast de rétroaction, puis naviguer dans l'application vers les tâches avec des rappels": c'est là que le la saga passe à l'action : orchestrer les composants

Voir aussi https://github.com/slorber/scalable-frontend-with-elm-or-redux

Et oui, de mon point de vue, c'est à vous en tant que développeur de déterminer quel est l'équilibre approprié pour la logique de votre propre application et où elle réside. Il n'y a pas de règle absolue pour savoir de quel côté de la division créateur/réducteur d'action il doit vivre.

Oui, Redux n'exige pas que vous mettiez votre logique dans les réducteurs ou les créateurs d'actions. Redux ne cassera pas de toute façon. Il n'y a pas de règle absolue qui vous oblige à le faire dans un sens ou dans l'autre. Mais la recommandation de Dan était de « Garder le journal des actions aussi près que possible de l'historique des interactions des utilisateurs ». L'envoi d'une seule action par événement utilisateur n'est pas obligatoire, mais il est recommandé.

Dans mon cas j'ai 2 réducteurs intéressés par 1 action. L'action.data brute n'est pas suffisante. Ils doivent gérer des données transformées. Je ne voulais pas faire la transformation dans les 2 réducteurs. J'ai donc déplacé la fonction pour effectuer la transformation en un thunk. De cette façon, mes réducteurs reçoivent une donnée prête à être consommée. C'est le meilleur que je puisse penser dans ma courte expérience de redux d'un mois.

Qu'en est-il du découplage des composants/vues de la structure du magasin ? mon objectif est que tout ce qui est affecté par la structure du magasin doit être géré dans les réducteurs, c'est pourquoi j'aime colocaliser les sélecteurs avec les réducteurs, donc les composants n'ont pas vraiment besoin de savoir comment obtenir un nœud particulier du magasin.

C'est très bien pour transmettre des données aux composants, qu'en est-il de l'inverse, lorsque les composants envoient des actions :

Disons par exemple que dans une application Todo, je mets à jour le nom d'un élément Todo, donc j'envoie une action en passant la partie de l'élément que je veux mettre à jour, c'est-à-dire :

dispatch(updateItem({name: <text variable>}));

, et la définition de l'action est :

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

qui à son tour est géré par le réducteur qui pourrait simplement faire :

Object.assign({}, item, action.updatedData)

pour mettre à jour l'élément.

Cela fonctionne très bien car je peux réutiliser la même action et le même réducteur pour mettre à jour n'importe quel accessoire de l'élément Todo, c'est-à-dire :

updateItem({description: <text variable>})

lorsque la description est modifiée à la place.

Mais ici, le composant doit savoir comment un élément Todo est défini dans le magasin et si cette définition change, je dois me rappeler de la modifier dans tous les composants qui en dépendent, ce qui est évidemment une mauvaise idée, des suggestions pour ce scénario ?

@dcoellarb

Ma solution dans ce genre de situation est de profiter de la flexibilité de Javascript pour générer ce qui serait passe-partout.

Donc j'aurais peut-être :

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

Où makeRecord est une fonction permettant de créer automatiquement des réducteurs, des créateurs d'actions et des sélecteurs à partir de ma description. Cela élimine le passe-partout, mais si je dois faire quelque chose qui ne correspond pas à ce modèle soigné plus tard, je peux ajouter un réducteur/actions/sélecteur personnalisé au résultat de makeRecord.

tks @winstonewert j'aime l'approche pour éviter le passe-partout, je peux voir que gagner beaucoup de temps dans les applications avec beaucoup de modèles; mais je ne vois toujours pas comment cela découplera le composant de la structure du magasin, je veux dire que même si l'action est générée, le composant devra toujours lui transmettre les champs mis à jour, ce qui signifie que le composant doit toujours connaître la structure de le magasin non?

@winstonewert @dcoellarb À mon avis, la structure de charge utile d'action devrait appartenir aux actions, pas aux réducteurs, et être explicitement traduite en structure d'état dans un réducteur. Ce devrait être une heureuse coïncidence que ces structures se reflètent l'une l'autre pour la simplicité initiale. Ces deux structures n'ont pas besoin de toujours se refléter car elles ne sont pas la même entité.

@sompylasar c'est ça , je fais ça, je traduis les données api/rest dans ma structure de magasin, mais le seul qui devrait connaître la structure du

@dcoellarb Vous pouvez considérer vos vues comme des entrées de données d'un certain type (comme une chaîne ou un nombre, mais un objet structuré avec des champs). Cet objet de données ne reflète pas nécessairement la structure du magasin. Vous mettez cet objet de données dans une action. Ce n'est pas un couplage avec la structure du magasin. Store et view doivent être couplés à une structure de charge utile d'action.

@sompylasar a du sens, je vais essayer, merci beaucoup !!!

Je devrais probablement aussi ajouter que vous pouvez rendre les actions plus pures en utilisant redux-saga . Cependant, redux-saga a du mal à gérer les événements asynchrones, vous pouvez donc aller plus loin dans cette idée en utilisant RxJS (ou n'importe quelle bibliothèque FRP) au lieu de redux-saga. Voici un exemple utilisant KefirJS : https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

Salut @frankandrobot ,

redux-saga a du mal à gérer les événements asynchrones

Que veux-tu dire par là? redux-saga n'est-il pas conçu pour gérer les événements asynchrones et les effets secondaires de manière élégante ? Jetez un œil à https://github.com/reactjs/redux/issues/1171#issuecomment-167715393

Non @IceOnFire . La dernière fois que j'ai lu les redux-saga docs, la gestion de workflows asynchrones complexes est difficile. Voir, par exemple : http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
Il a dit (dit encore ?) quelque chose à l'effet

on laisse le reste des détails au lecteur car ça commence à devenir complexe

Comparez cela avec la méthode FRP : https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a -more-complex-workflow
L'ensemble de ce flux de travail est entièrement géré. (En seulement quelques lignes, je pourrais ajouter.) En plus de cela, vous obtenez toujours la plupart des avantages de redux-saga (tout est une fonction pure, principalement des tests unitaires faciles).

La dernière fois que j'y ai pensé, je suis arrivé à la conclusion que le problème est que redux-saga fait que tout semble synchrone. C'est génial pour les workflows simples, mais pour les workflows asynchrones complexes, c'est plus facile si vous gérez explicitement l'async... c'est dans quoi FRP excelle.

Salut @frankandrobot ,

Merci pour votre explication. Je ne vois pas la citation que vous avez mentionnée, peut-être que la bibliothèque a évolué (par exemple, je vois maintenant un effet cancel que je n'avais jamais vu auparavant).

Si les deux exemples (saga et FRP) se comportent exactement de la même manière, je ne vois pas beaucoup de différence : l'un est une séquence d'instructions à l'intérieur des blocs try/catch, tandis que l'autre est une chaîne de méthodes sur les flux. En raison de mon manque d'expérience sur les streams, je trouve même plus lisible l'exemple de la saga, et plus testable puisque vous pouvez tester chaque yield un par un. Mais je suis sûr que cela est dû à mon état d'esprit plus qu'aux technologies.

En tout cas j'aimerais bien connaître l' avis de

@bvaughn pouvez-vous indiquer un exemple décent d'action de test, de réducteur, de sélecteur dans le même test que celui que vous décrivez ici ?

Le moyen le plus efficace de tester les actions, les réducteurs et les sélecteurs est de suivre l'approche « canards » lors de l'écriture des tests. Cela signifie que vous devez écrire un ensemble de tests qui couvre un ensemble donné d'actions, de réducteurs et de sélecteurs plutôt que 3 ensembles de tests qui se concentrent sur chacun individuellement. Cela simule plus précisément ce qui se passe dans votre application réelle et offre le meilleur rapport qualité-prix.

Salut @ morgs32

Ce problème est un peu ancien et je n'ai pas utilisé Redux depuis un moment. Il y a une section sur le site Redux sur l' écriture de tests que vous voudrez peut-être consulter.

Fondamentalement, je soulignais simplement que - plutôt que d'écrire des tests pour les actions et les réducteurs de manière isolée - il peut être plus efficace de les écrire ensemble, comme ceci :

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

Ce n'était que ma propre observation après avoir utilisé Redux pendant quelques mois sur un projet. Ce n'est pas une recommandation officielle. YMMV. ??

"Faire plus en action-créateurs et moins en réducteurs"

Que se passe-t-il si l'application est serveur et client et que le serveur doit contenir une logique métier et des validateurs ?
J'envoie donc l'action telle quelle, et la plupart des travaux seront effectués côté serveur par le réducteur...

Tout d'abord, désolé pour mon anglais. Mais j'ai des avis différents.

Mon choix est fat réducteur, thin créateurs d'action.

Mes créateurs d'action se contentent d'actions __dispatch__ (async, sync, serial-async, parallel-async, parallel-async in for-loop) basées sur certains __promise middleware__ .

Mes réducteurs se sont divisés en plusieurs petites tranches d'état pour gérer la logique métier. utilisez combineReduers combinez-les. reducer est __pure fonction__, il est donc facile à réutiliser. Peut-être qu'un jour j'utiliserai angularJS, je pense pouvoir réutiliser mes reducer dans mon service pour la même logique métier. Si votre reducer a de nombreux codes de lignes, il peut peut-être se diviser en un plus petit réducteur ou abstraire certaines fonctions.

Oui, il existe des cas d'état croisé dont les significations A dépendent de B, C .. et B, C sont des données asynchrones. Nous devons utiliser B,C pour remplir ou initialiser A. C'est pourquoi j'utilise crossSliceReducer .

À propos de __Faire plus dans les créateurs d'action et moins dans les réducteurs__.

  1. Si vous utilisez redux-thunk ou etc. Ouais. Vous pouvez accéder à l'état complet dans les créateurs d'action par getState() . C'est un choix. Ou, vous pouvez créer un __crossSliceReducer__, afin que vous puissiez également accéder à l'état complet, vous pouvez utiliser une tranche d'état pour calculer votre autre état.

À propos des __Tests unitaires__

reducer est une __fonction pure__. Il est donc facile de tester. Pour l'instant, je teste juste mes réducteurs, car c'est plus important que l'autre partie.

Pour tester action creators ? Je pense que s'ils sont "gros", ce n'est peut-être pas facile de les tester. Surtout les __créateurs d'action asynchrones__.

Je suis d'accord avec toi @mrdulin c'est maintenant comme ça que je suis parti aussi.

@mrdulin Ouais. Il semble que le middleware soit le bon endroit pour placer votre logique impure.
Mais pour la logique métier, le réducteur ne semble pas être le bon endroit. Vous vous retrouverez avec plusieurs actions "synthétiques" qui ne représentent pas ce que l'utilisateur a demandé mais ce que votre logique métier exige.

Un choix beaucoup plus simple consiste simplement à appeler des fonctions/méthodes de classe pures à partir du middleware :

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

@bvaughn

Cela réduit la probabilité de variables mal saisies qui conduisent à des valeurs indéfinies subtiles, cela simplifie les modifications apportées à la structure de votre magasin, etc.

Mon argument contre l'utilisation de sélecteurs partout, même pour des éléments d'état triviaux qui ne nécessitent pas de mémorisation ou de transformation de données :

  • Les tests unitaires pour les réducteurs ne devraient-ils pas déjà détecter les propriétés d'état mal saisies ?
  • D'après mon expérience, les state.stuff.item1 en state.stuff.item2 vous recherchez dans le code et changez-le partout - comme si vous changez le nom de n'importe quoi d'autre. C'est une tâche courante et une évidence pour les personnes utilisant des IDE en particulier.
  • L'utilisation de sélecteurs partout est une sorte d'abstraction vide. simples morceaux d'état. Bien sûr, vous gagnez en cohérence en ayant cette API pour accéder à l'état, mais vous renoncez à la simplicité.

Évidemment, les sélecteurs sont nécessaires, mais j'aimerais entendre d'autres arguments pour en faire une API obligatoire.

D'un point de vue pratique, une bonne raison d'après mon expérience est que les bibliothèques telles que reselect ou redux-saga utilisent des sélecteurs pour accéder à des éléments d'état. Cela me suffit pour m'en tenir aux sélecteurs.

D'un point de vue philosophique, je considère toujours les sélecteurs comme les "méthodes getter" du monde fonctionnel. Et pour la même raison pour laquelle je n'accéderais jamais aux attributs publics d'un objet Java, je n'accéderais jamais aux sous-états directement dans une application Redux.

@IceOnFire Il n'y a rien à exploiter si le calcul n'est pas coûteux ou si la transformation des données n'est pas nécessaire.

Les méthodes Getter peuvent être une pratique courante en Java, mais il en va de même pour l'accès aux POJO directement dans JS.

@timotgl

Pourquoi y a-t-il une API entre le magasin et un autre code redux ?

Les sélecteurs sont une API publique de requête (lecture) d'un réducteur, les actions sont une API publique de commande (écriture) d'un réducteur. La structure du réducteur est son détail de mise en œuvre.

Les sélecteurs et les actions sont utilisés dans la couche UI et dans la couche saga (si vous utilisez redux-saga), pas dans le réducteur lui-même.

@sompylasar Pas sûr de suivre votre point de vue ici. Il n'y a pas d'alternative aux actions, je dois les utiliser pour interagir avec redux. Cependant, je n'ai pas besoin d'utiliser de sélecteurs, je peux simplement choisir quelque chose directement dans l'état lorsqu'il est exposé, ce qui est le cas par conception.

Vous décrivez une façon de considérer les sélecteurs comme une API de "lecture" d'un réducteur, mais ma question était de savoir ce qui justifiait de faire des sélecteurs une API obligatoire en premier lieu (obligatoire comme pour l'appliquer en tant que meilleure pratique dans un projet, pas par bibliothèque conception).

@timotgl Oui, les sélecteurs ne sont pas obligatoires. Mais ils constituent une bonne pratique pour se préparer aux futures modifications du code de l'application, ce qui permet de les refactoriser séparément :

  • comment certaines parties de l'état sont structurées et écrites (la préoccupation du réducteur, un endroit)
  • comment cet élément d'état est utilisé (l'interface utilisateur et les effets secondaires, de nombreux endroits où le même élément d'état est interrogé)

Lorsque vous êtes sur le point de modifier la structure du magasin, sans sélecteurs, vous devrez rechercher et refactoriser tous les endroits où les éléments d'état affectés sont accessibles, et cela pourrait potentiellement être une tâche non triviale, pas simplement rechercher et - replace, surtout si vous faites circuler les fragments d'état obtenus directement depuis le store, et non via un sélecteur.

@sompylasar Merci pour votre contribution.

cela pourrait potentiellement être une tâche non triviale

C'est une préoccupation valable, cela me semble juste être un compromis coûteux. Je suppose que je n'ai pas rencontré de refactorisation d'état qui a causé de tels problèmes. Cependant, je suis tombé sur des "spaghettis de sélecteur", où des sélecteurs imbriqués pour chaque sous-élément d'état trivial ont causé toute la confusion. Cette contre-mesure elle-même doit également être maintenue après tout. Mais je comprends mieux la raison derrière cela maintenant.

@timotgl Un exemple simple que je peux partager publiquement :

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

Ce n'est pas la préoccupation de l'utilisateur de ce réducteur de savoir si ce qui est stocké est state, valueOrError ou autre chose. Exposés sont la chaîne d'état (enum), quelques vérifications fréquemment utilisées sur cet état, la valeur et l'erreur.

Cependant, je suis tombé sur des "spaghettis de sélecteur", où des sélecteurs imbriqués pour chaque sous-élément d'état trivial ont causé toute la confusion.

Si cette imbrication était causée par la mise en miroir de l'imbrication du réducteur (composition), ce n'est pas ce que je recommanderais, c'est le détail de l'implémentation de ce réducteur. En utilisant l'exemple ci-dessus, les réducteurs qui utilisent promiseReducer pour certaines parties de leur état exportent leurs propres sélecteurs nommés en fonction de ces parties. De plus, toutes les fonctions qui ressemblent à un sélecteur ne doivent pas être exportées et faire partie de l'API du réducteur.

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

Oh encore une chose que j'ai failli oublier. Les noms de fonction et les exportations/importations peuvent être minimisés correctement et en toute sécurité. Clés d'objet imbriquées - pas tellement, vous avez besoin d'un traceur de flux de données approprié dans le minificateur pour ne pas gâcher le code.

@timotgl : beaucoup de nos meilleures pratiques encouragées avec Redux consistent à essayer d'encapsuler la logique et le comportement liés à Redux.

Par exemple, vous pouvez envoyer des actions directement depuis un composant connecté, en faisant this.props.dispatch({type : "INCREMENT"}) . Cependant, nous déconseillons cela, car cela force le composant à "savoir" qu'il parle à Redux. Une façon plus idiomatique de faire les choses consiste à transmettre des créateurs d'action liés, de sorte que le composant puisse simplement appeler this.props.increment() , et peu importe que cette fonction soit un créateur d'action Redux lié, un rappel passé par un parent, ou une fonction fictive dans un test.

Vous pouvez également écrire à la main des types d'action partout, mais nous encourageons la définition de variables constantes afin qu'elles puissent être importées, tracées et réduire les risques de fautes de frappe.

De même, rien ne vous empêche d'accéder à state.some.deeply.nested.field dans vos fonctions ou thunks mapState . Mais, comme cela a déjà été décrit dans ce fil, cela augmente les risques de fautes de frappe, rend plus difficile la recherche des endroits où un élément d'état particulier est utilisé, rend la refactorisation plus difficile et signifie que toute logique de transformation coûteuse est probablement re -exécuter à chaque fois même si ce n'est pas nécessaire.

Donc non, vous n'avez pas besoin d'utiliser de sélecteurs, mais c'est une bonne pratique architecturale.

Vous voudrez peut-être lire mon article Idiomatic Redux: Using Resselect Selectors for Encapsulation and Performance .

@markerikson Je ne

Mon point était que je ne suis pas d'accord avec cette croyance:

Idéalement, seuls vos fonctions de réduction et vos sélecteurs devraient connaître la structure exacte de l'état. Par conséquent, si vous modifiez l'emplacement d'un état, vous n'aurez qu'à mettre à jour ces deux éléments de logique.

À propos de votre exemple avec state.some.deeply.nested.field , je peux voir l'intérêt d'avoir un sélecteur pour raccourcir cela. selectSomeDeeplyNestedField() est un nom de fonction contre 5 propriétés que je pourrais me tromper.

D'un autre côté, si vous suivez cette directive à la lettre, vous faites également const selectSomeField = state => state.some.field; ou même const selectSomething = state => state.something; , et à un moment donné la surcharge (importation, exportation, test) de le faire de manière cohérente ne justifie plus la sécurité et la pureté (discutables) à mon avis. C'est bien intentionné mais je ne peux pas me débarrasser de l'esprit dogmatique de la directive. Je ferais confiance aux développeurs de mon projet pour utiliser les sélecteurs à bon escient et le cas échéant - car mon expérience jusqu'à présent a été qu'ils le font.

Sous certaines conditions, je pouvais voir pourquoi vous voudriez pécher par excès de sécurité et de conventions. Merci pour votre temps et votre engagement, je suis globalement très heureux que nous ayons redux.

Sûr. FWIW, il existe d' autres bibliothèques de sélecteurs, ainsi que des wrappers autour de Reselect . Par exemple, https://github.com/planttheidea/selectorator vous permet de définir des chemins de clés de notation par points, et il effectue les sélecteurs intermédiaires pour vous en interne.

Cette page vous a été utile?
0 / 5 - 0 notes