Comment ajouter/supprimer à un magasin Redux généré avec Normalizr?
En regardant les exemples du README :
Compte tenu de la "mauvaise" structure:
[{
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 est quelque chose comme
return {
...state,
myNewObject
}
Dans le réducteur.
Maintenant, étant donné la structure du "bon" arbre, je n'ai aucune idée de la façon dont 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 à laquelle j'ai pensé nécessite une manipulation d'objet complexe, ce qui me donne l'impression que je ne suis pas sur la bonne voie, car normalizr est censé me faciliter la vie.
Je ne trouve aucun exemple en ligne montrant que quelqu'un travaille avec l'arbre normalizr de cette manière. L'exemple officiel n'ajoute ni ne supprime donc aucune aide.
Est-ce que quelqu'un pourrait me faire savoir comment ajouter/supprimer d'un arbre normalizr de la bonne façon?
Ce qui suit est directement tiré d'un message du créateur de redux/normalizr ici :
Donc, votre état ressemblerait à ceci:
{
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 à l'intérieur d'autres entités. Au lieu de cela, ils se référent les uns aux autres par identifiants. Ainsi, chaque fois qu'un objet change, il n'y a qu'un seul endroit où il doit être mis à jour.
Deuxièmement, remarquez 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 currentPlans. C'est important. Dans des applications plus complexes, vous pouvez avoir des relations, par exemple plans réducteur peut gérer ADD_EXERCISE_TO_PLAN de la même manière en ajoutant un nouvel ID au tableau à l'intérieur du plan. Mais si l'exercice est lui-même mis à jour, il n'est pas nécessaire que réducteur de plans le sache, car l'ID n'a pas changé.
Troisièmement, notez que les entités réductrices (plans et exercices) ont des clauses spéciales surveillant les actions. C’est au cas où nous aurions une réponse de serveur avec la «vérité connue» que nous voulons mettre à jour pour refléter toutes nos entités. Pour préparer vos données de cette manière avant d'envoyer une action, vous pouvez utiliser normalizr. Vous pouvez le voir utilisé dans l'exemple du «monde réel» dans le référentiel Redux.
Enfin, remarquez à quel point les réducteurs d’entités sont similaires. Vous voudrez peut-être écrire une fonction pour les générer. Cela dépasse le cadre de ma réponse: vous voulez parfois plus de flexibilité et parfois moins de standard. Vous pouvez consulter le code de pagination dans les exemples de réducteurs du «monde réel» pour obtenir un exemple de génération de réducteurs similaires.
Oh, et j'ai utilisé la syntaxe {... a, ... b}. Il est activé dans Babel stage 2 en tant que proposition ES7. Cela s'appelle «opérateur de propagation d'objet» et équivaut à écrire Object.assign ({}, a, b).
En ce qui concerne les bibliothèques, vous pouvez utiliser Lodash (attention à ne pas muter, par exemple, fusion ({}, a, b} est correct mais fusion (a, b) ne l’est pas), updeep, react-addons-update ou autre chose. Toutefois, si vous devez effectuer des mises à jour approfondies, cela signifie probablement que votre arborescence d'état n'est pas assez plate et que vous n'utilisez pas suffisamment de composition fonctionnelle.
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 issues d'une API, car je n'ai aucun contrôle sur les structures de données imbriquées (généralement) profondes. Différencions les entités et les résultats et leur utilisation.
Entités
Toutes les données pures se trouvent dans l'objet Entités après la normalisation (dans votre cas, articles
et users
). Je recommanderais soit d'utiliser un réducteur pour toutes les entités ou un réducteur pour chaque type d'entité. Le ou les réducteurs d'entité doivent être responsables de maintenir vos données (serveur) synchronisées et de disposer d'une source de vérité unique.
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 extrayez d'une API recommandée articles
avec ids: ['1', '2']
. Vous enregistrez les entités dans votre réducteur d'entité d'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 d'entité d'article. Le réducteur d'entité d'article} est l'unique source de vérité pour toutes vos données d'article - c'est tout. Vous souhaitez maintenant disposer d'un autre emplacement pour différencier les articles ((1) articles recommandés et (2) articles de 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 conservez qu'une seule source de vérité dans votre réducteur d'entité d'article.
Dans votre composant, vous pouvez simplement mapper des entités + recommandé/articlesByAuthor pour présenter l'entité.
Avertissement: je peux recommander un article de blog que j'ai écrit, qui montre comment une application réelle utilise normalizr pour éviter les problèmes de gestion d'état: Redux Normalizr: améliorez votre gestion d'état
J'ai mis en place une petite déviation d'un réducteur générique que l'on peut trouver sur Internet. Il est capable de supprimer des éléments du cache. Tout ce que vous avez à faire est de vous assurer qu'à chaque suppression, vous envoyez une action avec un 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 en code d'action:
await AlarmApi.remove(alarmId)
dispatch({
type: 'ALARM_DELETED',
alarmId,
deleted: {alarms: [alarmId]},
})
Dans votre réducteur, conservez une copie des données non normalisées. De cette façon, vous pouvez faire quelque chose comme ceci (lors de l'ajout d'un nouvel objet à un tableau en état):
case ACTION:
return {
unNormalizedData: [...state.unNormalizedData, action.data],
normalizedData: normalize([...state.unNormalizedData, action.data], normalizrSchema),
}
Si vous ne souhaitez pas conserver de données non normalisées dans votre magasin, vous pouvez également utiliser dénormaliser