Comment ajouter ou supprimer à un magasin redux généré avec normalisation?

En regardant les exemples du README :

Compte tenu de la structure "mauvaise":

[{ id: 1, title: 'Some Article', author: { id: 1, name: 'Dan' } }, { id: 2, title: 'Other Article', author: { id: 1, name: 'Dan' } }] 

Il est extrêmement facile d'ajouter un nouvel objet. Tout ce que je dois faire, c'est quelque chose comme

 return { ...state, myNewObject } 

Dans le réducteur.

Maintenant, compte tenu de la structure du «bon» arbre, je ne sais pas comment je devrais l'aborder.

 { result: [1, 2], entities: { articles: { 1: { id: 1, title: 'Some Article', author: 1 }, 2: { id: 2, title: 'Other Article', author: 1 } }, users: { 1: { id: 1, name: 'Dan' } } } } 

Chaque approche que j'ai pensé nécessite une manipulation complexe d'objets, ce qui me fait sentir que je ne suis pas sur la bonne voie parce que normalizr est censé rendre ma vie plus facile.

Je ne peux trouver aucun exemple en ligne de quelqu'un qui travaille avec l'arbre normalizr de cette manière. L'exemple officiel ne l'ajoute pas et ne l'en supprime pas.

Quelqu'un pourrait-il me dire comment ajouter / supprimer d'un arbre normalizr de la bonne manière?

Ce qui suit est directement à partir d'une publication par le créateur redux / normalizr ici :

Votre état ressemblerait donc à:

 { entities: { plans: { 1: {title: 'A', exercises: [1, 2, 3]}, 2: {title: 'B', exercises: [5, 1, 2]} }, exercises: { 1: {title: 'exe1'}, 2: {title: 'exe2'}, 3: {title: 'exe3'} } }, currentPlans: [1, 2] } 

Vos réducteurs pourraient ressembler

 import merge from 'lodash/object/merge'; const exercises = (state = {}, action) => { switch (action.type) { case 'CREATE_EXERCISE': return { ...state, [action.id]: { ...action.exercise } }; case 'UPDATE_EXERCISE': return { ...state, [action.id]: { ...state[action.id], ...action.exercise } }; default: if (action.entities && action.entities.exercises) { return merge({}, state, action.entities.exercises); } return state; } } const plans = (state = {}, action) => { switch (action.type) { case 'CREATE_PLAN': return { ...state, [action.id]: { ...action.plan } }; case 'UPDATE_PLAN': return { ...state, [action.id]: { ...state[action.id], ...action.plan } }; default: if (action.entities && action.entities.plans) { return merge({}, state, action.entities.plans); } return state; } } const entities = combineReducers({ plans, exercises }); const currentPlans = (state = [], action) { switch (action.type) { case 'CREATE_PLAN': return [...state, action.id]; default: return state; } } const reducer = combineReducers({ entities, currentPlans }); 

Alors, qu'est-ce qui se passe ici? Tout d'abord, notez que l'état est normalisé. Nous n'avons jamais d'entités dans d'autres entités. Au lieu de cela, ils se réfèrent par ID. Donc, chaque fois qu'un objet change, il n'y a qu'un seul endroit où il doit être mis à jour.

Deuxièmement, notez comment nous réagissons à CREATE_PLAN en ajoutant une entité appropriée dans le réducteur de plans et en ajoutant son ID au réducteur actuel. C'est important. Dans les applications plus complexes, vous pouvez avoir des relations, par exemple, un réducteur de plans peut gérer ADD_EXERCISE_TO_PLAN de la même manière en ajoutant une nouvelle ID au tableau dans le plan. Mais si l'exercice lui-même est mis à jour, il n'est pas nécessaire que les plans réducteurs le sachent, car l'ID n'a pas changé.

Troisièmement, notez que les réducteurs d'entités (plans et exercices) ont des clauses spéciales qui surveillent les actions. C'est au cas où nous avons une réponse du serveur avec "la vérité connue" que nous voulons mettre à jour toutes nos entités pour réfléchir. Pour préparer vos données de cette façon avant d'envoyer une action, vous pouvez utiliser normalizr. Vous pouvez le voir dans l'exemple du «monde réel» dans le repo de Redux.

Enfin, notez comment les réducteurs d'entités sont similaires. Vous voudrez peut-être écrire une fonction pour les générer. C'est hors de portée de ma réponse: parfois, vous voulez plus de souplesse, et parfois vous voulez moins de clarté. Vous pouvez vérifier le code de pagination dans les réducteurs d'exemple "en vrac" pour un exemple de génération de réducteurs similaires.

Oh, et j'ai utilisé {… a, … b} syntaxe. Il est permis dans Babel étape 2 en tant que proposition ES7. Il s'appelle "opérateur de propagation d'objet" et équivaut à l'écriture Object.assign ({}, a, b).

En ce qui concerne les bibliothèques, vous pouvez utiliser Lodash (veillez à ne pas muter, par exemple, fusionnez ({}, a, b} est correct, mais fusionnez (a, b) n'est pas), updeep, reac-addons-update ou autre chose. Cependant, si vous vous trouvez avoir besoin de faire des mises à jour importantes, cela signifie probablement que votre arbre d'état n'est pas assez plat et que vous n'utilisez pas suffisamment la composition fonctionnelle. Même votre premier exemple:

 case 'UPDATE_PLAN': return { ...state, plans: [ ...state.plans.slice(0, action.idx), Object.assign({}, state.plans[action.idx], action.plan), ...state.plans.slice(action.idx + 1) ] }; 

Peut être écrit comme

 const plan = (state = {}, action) => { switch (action.type) { case 'UPDATE_PLAN': return Object.assign({}, state, action.plan); default: return state; } } const plans = (state = [], action) => { if (typeof action.idx === 'undefined') { return state; } return [ ...state.slice(0, action.idx), plan(state[action.idx], action), ...state.slice(action.idx + 1) ]; }; // somewhere case 'UPDATE_PLAN': return { ...state, plans: plans(state.plans, action) }; 

La plupart du temps, j'utilise normalizr pour les données que je reçois d'une API, car je n'ai aucun contrôle sur les structures de données ancestrales (généralement). Distinguons les Entités et les Résultat et leur utilisation.

Entités

Toutes les données pures sont dans l'objet entités après sa normalisation (dans les articles et les users votre cas). Je recommanderais soit d'utiliser un réducteur pour toutes les entités, soit un réducteur pour chaque type d'entité. Le (s) réducteur (s) de l'entité devraient être responsables de conserver vos données (du serveur) en synchronisation et d'avoir une seule source de vérité.

 const initialState = { articleEntities: {}, userEntities: {}, }; 

Résultat

Les résultats ne sont que des références à vos entités. Imaginez le scénario suivant: (1) Vous recherchez un articles recommandé par l'API avec les ids: ['1', '2'] . Vous économisez les entités dans votre réducteur d'entité article . (2) Maintenant, vous récupérez tous les articles écrits par un auteur spécifique avec id: 'X' . Encore une fois, vous synchronisez les articles dans le réducteur de l'entité article . Le réducteur de l'entité article est la source unique de la vérité pour toutes vos données d'article – c'est ça. Maintenant, vous voulez avoir un autre endroit pour différencier les articles ((1) articles recommandés et (2) articles par l'auteur X). Vous pouvez facilement les conserver dans un autre réducteur spécifique à un cas d'utilisation. L'état de ce réducteur pourrait ressembler à ceci:

 const state = { recommended: ['1', '2' ], articlesByAuthor: { X: ['2'], }, }; 

Maintenant, vous pouvez facilement voir que l'article de l'auteur X est également un article recommandé. Mais vous ne gardez qu'une seule source de vérité dans votre réducteur d'entité article.

Dans votre composant, vous pouvez simplement mapper les entités + recommandé / articlesByAuthor pour présenter l'entité.

Disclaimer: je peux recommander une publication de blog que j'ai écrit, qui montre comment une application mondiale réelle utilise normalisation pour éviter les problèmes de gestion de l'état: Redux Normalizr: Améliorez votre gestion de l'état

J'ai mis en place une petite déviation d'un réducteur générique qui se trouve sur Internet. Il est capable de supprimer des objets du cache. Tout ce que vous devez faire est de vous assurer que chaque suppression vous envoie une action avec le champ supprimé:

 export default (state = entities, action) => { if (action.response && action.response.entities) state = merge(state, action.response.entities) if (action.deleted) { state = {...state} Object.keys(action.deleted).forEach(entity => { let deleted = action.deleted[entity] state[entity] = Object.keys(state[entity]).filter(key => !deleted.includes(key)) .reduce((p, id) => ({...p, [id]: state[entity][id]}), {}) }) } return state } 

Exemple d'utilisation dans le code d'action:

 await AlarmApi.remove(alarmId) dispatch({ type: 'ALARM_DELETED', alarmId, deleted: {alarms: [alarmId]}, })