Expérience de l'utilisation de redux sans réducteurs



Je voudrais partager mon expérience de l'utilisation de redux dans une application d'entreprise. En parlant de logiciels d'entreprise dans le cadre de l'article, je me concentre sur les fonctionnalités suivantes:

  • Tout d'abord, c'est le volume de fonctionnalités. Ce sont des systèmes développés depuis de nombreuses années, continuant à construire de nouveaux modules, ou compliquant indéfiniment ce qui existe déjà.
  • Deuxièmement, souvent, si nous ne considérons pas un écran de présentation, mais le lieu de travail de quelqu'un, un grand nombre de composants attachés peuvent être montés sur une seule page.
  • Troisièmement, la complexité de la logique métier. Si nous voulons obtenir une application réactive et agréable à utiliser, une partie importante de la logique devra être faite par le client.

Les deux premiers points imposent des restrictions sur la marge de productivité. Plus d'informations à ce sujet plus tard. Et maintenant, je propose de discuter des problèmes que vous rencontrez en utilisant le redux-workflow classique, en développant quelque chose de plus compliqué que la liste TODO.

Redux classique


Pour un exemple, considérons l'application suivante:

image

L'utilisateur conduit une rime - obtient une évaluation de son talent. Le contrôle avec l'introduction du verset est contrôlé et le recalcul de l'évaluation a lieu pour chaque changement. Il y a aussi un bouton par lequel le texte avec le résultat est réinitialisé, et un message est affiché à l'utilisateur qu'il peut recommencer depuis le début. Code source dans ce fil .

Organisation du code:

image

Il y a deux modules. Plus précisément, un module est directement poemScoring. Et la racine de l'application avec des fonctions communes à l'ensemble du système est l'application. Là, nous avons des informations sur l'utilisateur, affichant des messages à l'utilisateur. Chaque module a ses propres réducteurs, actions, contrôles, etc. Au fur et à mesure que l'application se développe, les nouveaux modules se multiplient.

Une cascade de réducteurs, utilisant redux-immuable, forme l'état entièrement immuable suivant:

image

Comment ça marche:

1. Contrôlez l'action-créateur d'action:

import at from '../constants/actionTypes'; export function poemTextChange(text) { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: text }); }; } 

Les constantes des types d'actions sont déplacées vers un fichier distinct. Premièrement, nous sommes tellement à l'abri des fautes de frappe. Deuxièmement, l'intellisense sera à notre disposition.

2. Ensuite, il s'agit du réducteur.

 import logic from '../logic/poem'; export default function poemScoringReducer(state = Immutable.Map(), action) { switch (action.type) { case at.POEM_TYPE: return logic.onType(state, action.payload); default: return state; } } 

Le traitement logique est déplacé vers une fonction de cas distinct. Sinon, le code réducteur deviendra rapidement illisible.

3. La logique du traitement des clics en utilisant l'analyse lexicale et l'intelligence artificielle:

 export default { onType(state, text) { return state .set('poemText', text) .set('score', this.calcScore(text)); }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; } }; 

Dans le cas du bouton «Nouveau poème», nous avons le créateur d'action suivant:

 export function newPoem() { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: '' }); dispatch({ type: appAt.SHOW_MESSAGE, payload: 'You can begin a new poem now!' }); }; } 

Tout d'abord, envoyez la même action qui réinitialise notre texte et marque. Ensuite, envoyez l'action, qui sera interceptée par un autre réducteur et afficher un message à l'utilisateur.
Tout est beau. Créons nous-mêmes des problèmes:

Les problèmes:


Nous avons posté notre candidature. Mais nos utilisateurs, voyant qu'on leur a demandé d'écrire de la poésie, ont naturellement commencé à publier leur travail, ce qui est incompatible avec les normes d'entreprise du langage poétique. En d'autres termes, nous devons modérer les mots obscènes.

Ce que nous ferons:

  • dans le texte d'entrée, il est nécessaire de remplacer tous les mots non cultivés par * censuré *
  • en outre, si l'utilisateur a prononcé un gros mot, vous devez l'avertir par un message qu'il fait mal.

Bon. Il suffit d'analyser le texte, en plus de calculer le score, pour remplacer les mauvais mots. Pas de problème. Et aussi, pour informer l'utilisateur, vous avez besoin d'une liste de ce que nous avons supprimé. Le code source est ici .

On refait la fonction de la logique pour qu'elle, en plus du nouvel état, renvoie les informations nécessaires au message à l'utilisateur (mots remplacés):

 export default { onType(state, text) { const { reductedText, censoredWords } = this.redactText(text); const newState = state .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); return { newState, censoredWords }; }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; }, redactText(text) { const result = { reductedText:text }; const censoredWords = []; obscenseWords.forEach((badWord) => { if (result.reductedText.indexOf(badWord) >= 0) { result.reductedText = result.reductedText.replace(badWord, '*censored*'); censoredWords.push(badWord); } }); if (censoredWords.length > 0) { result.censoredWords = censoredWords.join(' ,'); } return result; } }; 

Appliquons-le maintenant. Mais comment? Dans le réducteur, cela n'a plus de sens pour nous de l'appeler, car nous mettrons le texte et l'évaluation en l'état, mais que devons-nous faire du message? Afin d'envoyer un message, dans tous les cas, nous devrons envoyer l'action correspondante. Nous finalisons donc l'action-créateur.

 export function poemTextChange(text) { return function (dispatch, getState) { const globalState = getState(); const scoringStateOld = globalState.get('poemScoring'); //        const { newState, censoredWords } = logic.onType(scoringStateOld, text); dispatch({ //        type: at.POEM_TYPE, payload: newState }); if (censoredWords) { //    ,    const userName = globalState.getIn(['app', 'account', 'name']); const message = `${userName}, avoid of using word ${censoredWords}, please!`; dispatch({ type: appAt.SHOW_MESSAGE, payload: message }); } }; } 

Il est également nécessaire de modifier le réducteur, car il n'appelle plus la fonction logique:

  switch (action.type) { case at.POEM_TYPE: return action.payload; default: return state; 

Que s'est-il passé:

image

Et maintenant, la question est. Pourquoi avons-nous besoin d'un réducteur qui, pour la plupart, retournera simplement la charge utile au lieu d'un nouvel état? Lorsque d'autres actions apparaissent qui traitent la logique de l'action, sera-t-il nécessaire d'enregistrer un nouveau type d'action? Ou peut-être créer un SET_STATE commun? Probablement pas, car alors, l'inspecteur sera un gâchis. Nous allons donc produire le même type de boîtier?

L'essence du problème est la suivante. Si le traitement de la logique implique de travailler avec un morceau d'état, dont plusieurs réducteurs sont responsables, alors vous devez écrire toutes sortes de perversions. Par exemple, les résultats intermédiaires des fonctions de cas, qui doivent ensuite être dispersées sur différents réducteurs à l'aide de plusieurs actions.

Une situation similaire, si la fonction case a besoin de plus d'informations que ce qui se trouve dans votre réducteur, vous devez faire son appel à l'action, où il y a accès à l'état global, puis envoyer le nouvel état en tant que charge utile. Un réducteur devra être divisé dans tous les cas, s'il y a beaucoup de logique dans le module. Et cela crée un grand inconvénient.

Regardons la situation d'un côté. Dans notre action, nous obtenons un morceau d'état du global. Ceci est nécessaire pour le muter ( globalState.get ('poemScoring'); ). Il s'avère que nous savons déjà en action avec quel état du travail se déroule. Nous avons un nouvel état. Nous savons où le mettre. Mais au lieu de le mettre dans un global, nous l'exécutons avec une sorte de constante de texte tout au long de la cascade de réducteurs afin qu'il passe par chaque commutateur et se substitue une fois. Moi dès la réalisation de cela, les rides. Je comprends que cela est fait pour faciliter le développement et réduire la connectivité. Mais dans notre cas, il n'a plus de rôle.

Maintenant, je vais énumérer tous les points que je n'aime pas dans l'implémentation actuelle, si elle doit être mise à l'échelle en profondeur et en profondeur pour une durée illimitée :

  1. Inconvénient important lorsque vous travaillez avec un état extérieur au réducteur.
  2. Le problème de la séparation des codes. Chaque fois que nous envoyons une action, elle passe par chaque réducteur, passe par chaque cas. Il est pratique de ne pas déranger lorsque vous avez une petite application. Mais, si vous avez un monstre qui a été construit pendant plusieurs années avec des dizaines de réducteurs et des centaines de cas, alors je commence à réfléchir à la faisabilité d'une telle approche. Peut-être, même avec des milliers de cas, cela n'aura pas d'impact significatif sur les performances. Mais, sachant qu'en imprimant du texte, chaque presse provoquera un passage à travers des centaines de cas, je ne peux pas le laisser tel quel. Tout, le moindre décalage, multiplié par l'infini, tend vers l'infini. En d'autres termes, si vous n'y pensez pas, tôt ou tard, des problèmes surgiront.

    Quelles sont les options?

    a. Applications isolées avec leurs propres fournisseurs . Dans chaque module (sous-application), vous devrez dupliquer les parties générales de l'état (compte, messages, etc.).

    b. Utilisez des réducteurs asynchrones enfichables. Ce n'est pas recommandé par Dan lui-même.

    c. Utilisez des filtres d'action dans les réducteurs. En d'autres termes, chaque envoi doit être accompagné d'informations sur le module auquel il est envoyé. Et dans les réducteurs de racine des modules, écrivez les conditions appropriées. J'ai essayé. Il n'y a pas eu autant d'erreurs involontaires ni avant ni après. Il y a une confusion constante sur la destination de l'action.
  3. Chaque fois qu'une action est envoyée, il n'y a pas seulement une exécution pour chaque réducteur, mais également la collecte de l'état inverse. Peu importe si l'état a changé dans le réducteur - il sera remplacé dans combineReducers.
  4. Chaque répartition oblige mapStateToProps à être traité pour chaque composant attaché monté sur la page. Si nous divisons les réducteurs, nous devons diviser les répartitions. Est-il essentiel que nous ayons un bouton qui écrase le texte et affiche le message avec différentes dépêches? Probablement pas. Mais j'ai une expérience en optimisation, lorsque la réduction du nombre d'envois de 15 à 3 a permis d'augmenter considérablement la réactivité du système, avec la même quantité de logique métier traitée. Je sais qu'il existe des bibliothèques qui peuvent combiner plusieurs dépêches en un seul lot, mais c'est un problème avec l'enquête à l'aide de béquilles.
  5. Lors du broyage de dépêches, il est parfois très difficile de voir ce qui se passe. Il n'y a pas un seul endroit, tout est dispersé sur différents fichiers. Il est nécessaire de rechercher où le traitement est mis en œuvre en recherchant des constantes dans tous les codes source.
  6. Dans le code ci-dessus, les composants et les actions accèdent directement à l'état global:

     const userName = globalState.getIn(['app', 'account', 'name']); … const text = state.getIn(['poemScoring', 'poemText']); 

    Ce n'est pas bon pour plusieurs raisons:

    a. Les modules devraient idéalement être isolés. Ils n'ont pas besoin de savoir où dans l'État où ils vivent.

    b. La mention des mêmes chemins à différents endroits est souvent lourde non seulement d'erreurs / fautes de frappe, mais rend également la refactorisation extrêmement difficile en cas de changement dans la configuration d'un état global, ou de changement dans la façon dont il est stocké.
  7. De plus en plus, en écrivant une nouvelle action, j'ai eu l'impression que j'écrivais du code pour le plaisir. Supposons que nous voulions ajouter une case à cocher à la page et refléter son état booléen dans l'histoire. Si nous voulons une organisation uniforme de l'action / des réducteurs, alors nous devons:

    - Enregistrer une constante de type action
    - Écrire un cratère d'action
    - Dans le contrôle, importez-le et enregistrez-le dans mapDispatchToProps
    - Inscrivez-vous dans PropTypes
    - Créez un handleCheckBoxClick dans le contrôle et spécifiez-le dans la case à cocher
    - Ajouter un interrupteur dans le réducteur avec un appel de fonction cas
    - Écrire une fonction de cas dans la logique

    Pour un seul chèque de boxe!
  8. L'état généré avec combineReducers est statique. Peu importe que vous ayez déjà entré ou non le module B, cette pièce sera dans l'histoire. Vide, mais le sera. Il n'est pas pratique d'utiliser l'inspecteur lorsqu'il y a beaucoup de nœuds vides inutilisés dans le steet.

Comment nous essayons de résoudre certains des problèmes décrits ci-dessus


Donc, nous avons eu des réducteurs stupides, et dans l'action-craters / logic nous écrivons des morceaux de code pour travailler avec des structures immuables profondément intégrées. Pour m'en débarrasser, j'utilise le mécanisme des sélecteurs hiérarchiques, qui permettent non seulement d'accéder à l'état souhaité, mais aussi de le remplacer (setIn pratique). Je l'ai publié dans le package immutable-selectors .

Regardons notre exemple comment cela fonctionne ( référentiel ):
Dans le module poemScoring, nous décrivons l'objet sélecteurs. Nous décrivons ces champs de l'état auquel nous voulons avoir un accès direct en lecture / écriture. L'imbrication et les paramètres d'accès aux éléments des collections sont autorisés. Il n'est pas nécessaire de décrire tous les champs possibles dans notre article.

 import extendSelectors from 'immutable-selectors'; const selectors = { poemText:{}, score:{} }; extendSelectors(selectors, [ 'poemScoring' ]); export default selectors; 

De plus, la méthode extendSelectors transforme chaque champ de notre objet en fonction de sélecteur. Le deuxième paramètre indique le chemin d'accès à la partie de l'état contrôlée par le sélecteur. Nous ne créons pas un nouvel objet, mais changeons l'objet actuel. Cela nous donne un bonus sous forme d'intelligence de travail:

image

Quel est notre objet - un sélecteur après son expansion:

image

La fonction selectors.poemText (état) exécute simplement state.getIn (['poemScoring', 'poemText']) .

Fonction root (état) - obtient 'poemScoring'.

Chaque sélecteur a sa propre fonction de remplacement (globalState, newPart) , qui, via setIn, renvoie un nouvel état global avec la pièce correspondante remplacée.

De plus, un objet plat est ajouté auquel toutes les touches de sélection uniques sont dupliquées. Autrement dit, si nous utilisons un état profond de la forme

 selectors = { dive:{ in:{ to:{ the:{ deep:{} } } } }} 

Vous pouvez approfondir en tant que selectors.dive.in.to the.deep (état) ou en tant que selectors.flat.deep (état) .

Allez-y. Nous devons mettre à jour l'acquisition des données dans les contrôles:

Poème:
 function mapStateToProps(state, ownprops) { return { text:selectors.poemText(state) || '' }; } 


Score:
 function mapStateToProps(state, ownprops) { const score = selectors.score(state); return { score }; } 

Ensuite, changez le réducteur de racine:

 import initialState from './initialState'; function setStateReducer(state = initialState, action) { if (action.setState) { return action.setState; } else { return state; // return combinedReducers(state, action); // } } export default setStateReducer; 

Si vous le souhaitez, nous pouvons combiner en utilisant combineReducers.

Cratère d'action, par exemple, poemTextChange:

 export function poemTextChange(text) { return function (dispatch, getState) { dispatch({ type: 'Poem typing', setState: logic.onType(getState(), text), payload: text }); }; } 

Nous ne pouvons plus utiliser de constantes de type action, car le type est désormais utilisé uniquement pour la visualisation dans l'inspecteur. Dans le projet, nous écrivons des descriptions en texte intégral de l'action en russe. Vous pouvez également vous débarrasser de la charge utile, mais j'essaie de l'enregistrer afin que dans l'inspecteur, si nécessaire, je comprenne avec quels paramètres l'action a été appelée.

Et, en fait, la logique elle-même:

  onType(gState, text) { const { reductedText, censoredWords } = this.redactText(text); const poemState = selectors.root(gState) || Immutable.Map(); //     const newPoemState = poemState //  .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); let newGState = selectors.root.replace(gState, newPoemState); //    if (censoredWords) { //  ,    const userName = appSelectors.flat.userName(gState); const messageText = `${userName}, avoid of using word ${censoredWords}, please!`; newGState = message.showMessage(newGState, messageText); } return newGState; }, 

Dans le même temps, message.showMessage est importé de la logique du module voisin, qui décrit ses sélecteurs:

  showMessage(gState, text) { return selectors.message.text.replace(gState, text); }. 

Ce qui s'avère:

image

Notez que nous avons eu un envoi, les données de deux modules ont changé.
Tout cela nous a permis de nous débarrasser des réducteurs et des constantes de type action, ainsi que de résoudre ou de contourner la plupart des goulots d'étranglement décrits ci-dessus.

Sinon, comment cela peut-il être appliqué?


Cette approche est pratique à utiliser lorsqu'il est nécessaire de s'assurer que vos commandes ou modules fournissent un travail avec différents éléments d'état. Disons qu'un poème ne nous suffit pas. Nous voulons que l'utilisateur puisse composer des poèmes sur deux onglets différents dans différentes disciplines (enfants, romantiques). Dans ce cas, nous ne pouvons pas importer les sélecteurs dans la logique / les commandes, mais les spécifier en tant que paramètre dans la commande externe:

  <Poem selectors = {selectors.hildPoem}/> <Poem selectors = {selectors.romanticPoem}/> 

Et, en outre, passez ce paramètre aux cratères d'action. Cela suffit pour faire une combinaison complexe de composants et de logique complètement enfermée, ce qui facilite la réutilisation.

Limitations lors de l'utilisation de sélecteurs immuables:

Il ne fonctionnera pas pour utiliser la clé dans l'état "nom", car pour la fonction parent, il y aura une tentative de remplacer la propriété réservée.

Quel est le résultat


En conséquence, une approche assez flexible a été obtenue, les relations de code implicites par les constantes de texte ont été éliminées, la surcharge a été réduite tout en conservant la commodité du développement. Il existe également un inspecteur redux pleinement opérationnel avec la possibilité de voyager dans le temps. Je n'ai aucune envie de revenir aux réducteurs standards.

En général, c'est tout. Merci pour votre temps. Peut-être que quelqu'un sera intéressé pour l'essayer!

Source: https://habr.com/ru/post/fr424613/


All Articles