Donc j'ai cette structure imbriquée sous mon état
state = {
plans: [
{title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
{title: 'B', exercises: [{title: 'exe5'}, {title: 'exe1'},{title: 'exe2'}]}
]
}
J'essaie de créer une réduction qui ne mute pas l'état précédent, mais j'arrive à un point où je passe plus de temps à comprendre comment faire cela, puis à coder le reste de l'application, cela devient assez frustrant.
Par exemple, si je voulais ajouter un nouvel exercice vide ou mettre à jour un exercice existant, muter les données, je ferais simplement:
state.plans[planIdx].exercises.push({})
state.plans[planIdx].exercises[exerciseIdx] = exercise
mais quelle pourrait être ma meilleure approche pour faire de même dans cette structure imbriquée? J'ai lu la documentation Redux et aussi la partie dépannage, mais le plus loin que j'ai obtenu était de mettre à jour un plan, où je ferais:
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)
]
};
N'y a-t-il pas un moyen plus rapide de travailler avec cela? Même si je dois utiliser des bibliothèques externes, ou du moins si quelqu'un peut m'expliquer comment mieux gérer cela ...
Je vous remercie!
Il est recommandé de normaliser JSON imbriqué comme: https://github.com/gaearon/normalizr
Oui, nous vous suggérons de normaliser vos données.
De cette façon, vous n'avez pas besoin d'aller «en profondeur»: toutes vos entités sont au même niveau.
Donc votre état ressemblerait à
{
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 des identifiants. Ainsi, 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 à la fois une entité appropriée dans le réducteur plans
_et_ en ajoutant son ID au réducteur currentPlans
. C'est important. Dans les applications plus complexes, vous pouvez avoir des relations, par exemple plans
reducer 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 lui-même est mis à jour, _il n'y a pas besoin de plans
reducer pour le savoir, car l'ID n'a pas changé_.
Troisièmement, notez que les réducteurs d'entités ( plans
et exercises
) ont des clauses spéciales pour surveiller action.entities
. C'est au cas où nous aurions une réponse du serveur avec une «vérité connue» que nous voulons mettre à jour toutes nos entités pour refléter. 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 repo 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. C'est hors de portée de ma réponse - parfois vous voulez plus de flexibilité, et parfois vous voulez moins de passe-partout. Vous pouvez consulter le code de pagination dans des exemples de réducteurs «du monde réel» pour un exemple de génération de réducteurs similaires.
Oh, et j'ai utilisé la syntaxe { ...a, ...b }
. Il est activé dans Babel étape 2 en tant que proposition ES7. Il 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 (faites attention à ne pas muter, par exemple merge({}, a, b}
est correct mais merge(a, b)
ne l'est pas), updeep , -addons-update ou autre chose. Cependant, si vous avez besoin de faire des mises à jour approfondies, cela signifie probablement que votre arborescence d'états n'est pas assez plate 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)
};
Ce serait bien d'en faire une recette.
Idéalement, nous voulons un exemple de tableau Kanban.
C'est parfait pour les entités imbriquées car les «voies» peuvent contenir des «cartes».
@ andre0799 ou vous pouvez simplement utiliser Immutable.js))
Idéalement, nous voulons un exemple de tableau Kanban.
J'en ai écrit un . Peut-être pourriez-vous le modifier et l'ajuster à votre guise.
Immutable.js n'est pas toujours une bonne solution. Il recalcule le hachage de chaque nœud d'état parent à partir du nœud que vous avez modifié et cela devient un goulot d'étranglement dans des cas particuliers (ce n'est pas des cas très courants). Donc, idéalement, vous devriez faire quelques benchmarks avant d'intégrer Immutable.js dans votre application.
Merci @gaearon pour votre réponse, bonne explication!
Ainsi, lorsque vous faites CREATE_PLAN
vous devez créer automatiquement un exercice par défaut et y ajouter. Comment dois-je gérer des cas comme celui-ci? Dois-je alors appeler 3 actions consécutives? CREATE_PLAN, CREATE_EXERCISE, ADD_EXERCISE_TO_PLAN
D'où dois-je faire ces appels, si tel est le cas?
Ainsi, lorsque vous effectuez CREATE_PLAN, vous devez créer automatiquement un exercice par défaut et y ajouter. Comment dois-je gérer des cas comme celui-ci?
Alors que je suis généralement en faveur de nombreux réducteurs gérant la même action, cela peut devenir trop compliqué pour les entités avec des relations. En effet, je suggère de modéliser ces actions comme des actions distinctes.
Vous pouvez utiliser le middleware Redux Thunk pour écrire un créateur d'action qui les appelle tous les deux:
function createPlan(title) {
return dispatch => {
const planId = uuid();
const exerciseId = uuid();
dispatch({
type: 'CREATE_EXERCISE',
id: exerciseId,
exercise: {
id: exerciseId,
title: 'Default'
}
});
dispatch({
type: 'CREATE_PLAN',
id: planId,
plan: {
id: planId,
exercises: [exerciseId],
title
}
});
};
}
Ensuite, si vous appliquez le middleware Redux Thunk, vous pouvez l'appeler normalement:
store.dispatch(createPlan(title));
Alors disons que j'ai un éditeur de publication sur le backend quelque part avec de telles relations (publications, auteurs, balises, pièces jointes, etc.).
Comment puis-je afficher currentPosts
similaire au tableau currentPlans
de clés? Dois-je mapper chaque clé de currentPosts
à son objet correspondant dans entities.posts
dans la fonction mapStateToProps
? Que diriez-vous de trier currentPosts
?
Tout cela appartient-il à une composition réductrice?
Il me manque quelque chose ici ...
En ce qui concerne la question initiale, je pense que les React Immutability Helpers ont été créés à cet effet
Comment afficher currentPosts similaire au tableau de clés currentPlans? Aurais-je besoin de mapper chaque clé dans currentPosts à son objet correspondant dans entity.posts dans la fonction mapStateToProps? Que diriez-vous de trier les messages actuels?
C'est correct. Vous feriez tout lors de la récupération des données. Veuillez consulter les exemples «panier» et «monde réel» fournis avec Redux repo.
Merci, j'ai déjà commencé à me faire une idée après avoir lu Computing Derived Data sur la documentation.
Je vais revoir ces exemples. Je n'ai probablement pas compris beaucoup de ce qui se passait au début quand je les ai lus.
@ andre0799
Tout composant connect()
ed a dispatch
injecté comme accessoire par défaut.
this.props.dispatch(createPlan(title));
Il s'agit d'une question d'utilisation qui n'est pas liée à ce fil. Il est préférable de consulter des exemples ou de créer des questions StackOverflow pour cela.
Je suis d'accord avec Dan pour normaliser les données et aplatir autant que possible la structure de l'état. Cela peut être mis comme recette / meilleure pratique dans la documentation, car cela m'aurait évité quelques maux de tête.
Comme j'ai commis l'erreur d'avoir un peu de profondeur dans mon état, j'ai créé cette bibliothèque pour aider à la modernisation et à la gestion de l'état profond avec Redux: https://github.com/baptistemanson/immutable-path
Peut-être que je me suis trompé, mais je serais intéressé par vos commentaires. J'espère que cela aidera quelqu'un.
Cela a été utile. Merci à tous.
Comment allez-vous ajouter un exercice à un plan, en utilisant la même structure que l'exemple du monde réel? Disons que l'ajout d'un exercice a renvoyé l'entité d'exercice nouvellement créée, avec un champ planId
. Est-il possible d'ajouter le nouvel exercice à ce plan sans avoir à écrire un réducteur pour les plans et d'écouter spécifiquement une action CREATE_EXERCISE
?
Excellente discussion et informations ici. Je voudrais utiliser normalizr pour mon projet, mais j'ai une question concernant la sauvegarde des données mises à jour sur un serveur distant. Principalement, existe-t-il un moyen simple de rétablir la forme normalisée à la forme imbriquée fournie par l'API distante après la mise à jour? Ceci est important lorsque le client apporte des modifications et doit les renvoyer à l'API distante où il n'a aucun contrôle sur la forme de la demande de mise à jour.
Par exemple: le client récupère les données d'exercice imbriquées -> le client les normalise et les stocke dans un redux -> l'utilisateur apporte des modifications aux données normalisées côté client -> l'utilisateur clique sur enregistrer -> l'application client transforme les données normalisées mises à jour au format imbriqué afin qu'il puisse le soumettre au serveur distant -> le client soumet au serveur
Si j'utilisais normalizr, aurais-je besoin d'écrire un transformateur personnalisé pour l'étape en gras ou y a-t-il une bibliothèque ou une méthode d'aide que vous recommanderiez pour cela? Toutes les recommandations seraient très appréciées.
Merci
Il y a quelque chose qui s'appelle https://github.com/gpbl/denormalizr mais je ne suis pas sûr à quel point il suit les mises à jour de normalizr. J'ai écrit normalizr en quelques heures pour l'application sur laquelle j'ai travaillé, vous êtes invités à la bifurquer et à ajouter la dénormalisation 😄.
Cool, je vais certainement jeter un oeil à la dénormalisation et contribuer à votre projet une fois que j'aurai quelque chose. Excellent travail pendant quelques heures ;-) Merci de me répondre.
Je suis un peu dans la même situation avec la modification des données profondément imbriquées, cependant, j'ai trouvé une solution de travail en utilisant immutable.js.
Est-ce que je peux créer un lien vers une publication StackOverflow que j'ai faite pour demander des commentaires sur la solution ici?
Je le lie pour le moment, veuillez supprimer mon message ou indiquer si le lien n'est pas approprié ici:
http://stackoverflow.com/questions/37171203/manipulating-data-in-nested-arrays-in-redux-with-immutable-js
J'ai vu cette approche être recommandée dans le passé. Cependant, cette approche ne semble pas bien fonctionner lorsqu'un objet imbriqué doit être supprimé. Dans ce cas, le réducteur aurait besoin de parcourir toutes les références à l'objet et de les supprimer, une opération qui serait O (n), avant de pouvoir supprimer l'objet lui-même. Quelqu'un a rencontré un problème similaire et l'a résolu?
@ariofrio : ah ... je suis confus. Le point de normalisation est que les objets ne sont _pas_ stockés imbriqués, et qu'il n'y a qu'une seule référence à un élément donné, ce qui facilite la mise à jour de cet élément. Maintenant, s'il y a plusieurs autres entités qui "référençaient" cet élément par ID, bien sûr, elles devraient également être mises à jour, comme si les choses n'étaient pas normalisées.
Avez-vous une préoccupation particulière ou un scénario problématique auquel vous faites face?
Voici ce que je veux dire. Disons que l'état actuel ressemble à:
{
entities: {
plans: {
1: {title: 'A', exercises: [1, 2, 3]},
2: {title: 'B', exercises: [5, 6]}
},
exercises: {
1: {title: 'exe1'},
2: {title: 'exe2'},
3: {title: 'exe3'}
5: {title: 'exe5'}
6: {title: 'exe6'}
}
},
currentPlans: [1, 2]
}
Dans cet exemple, chaque exercice ne peut être référencé que par un plan. Lorsque l'utilisateur clique sur "Supprimer l'exercice", le message peut ressembler à ceci:
{type: "REMOVE_EXERCISE", payload: 2}
Mais pour l'implémenter correctement, il faudrait parcourir tous les plans, puis tous les exercices dans chaque plan, pour trouver celui qui fait référence à l'exercice avec l'ID 2, afin d'éviter une référence pendante. C'est l'opération O (n) qui m'inquiétait.
Un moyen d'éviter cela est d'inclure l'ID du plan dans la charge utile de REMOVE_EXERCISE, mais à ce stade, je ne vois pas l'avantage d'utiliser l'imbrication des structures. Si nous avons utilisé l'état imbriqué à la place, l'état pourrait ressembler à:
{
plans: [
{title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
{title: 'B', exercises: [{title: 'exe5'}, {title: 'exe6'}]}
]
}
Et le message pour supprimer l'exercice pourrait ressembler à:
{type: "REMOVE_EXERCISE", payload: {plan_index: 0, exercise_index: 1}}
Quelques réflexions:
{id, planId, exerciseId}
. Certainement un scan O (n), mais simple.En fin de compte, l'état imbriqué et normalisé sont des conventions. Il y a de bonnes raisons d'utiliser l'état normalisé avec Redux, et il peut y avoir de bonnes raisons de garder votre état normalisé. Choisissez ce qui fonctionne pour vous :)
ma solution comme ceci:
function deepCombinReducer(parentReducer, subReducer) {
return function (state = parentReducer(void(0) /* get parent reducer initial state */, {}) {
let finalState = {...state};
for (var k in subReducer) {
finalState[k] = subReducer(state[k], action);
}
return parentReducer(finalState, action);
};
}
const parentReducer = function(state = {}, action) {
return state;
}
const subReducer = function(state = [], action) {
state = Immutable.fromJS(state).toJS();
switch(action.type) {
case 'ADD':
state.push(action.sub);
return state;
default:
return state;
}
}
export default combineReducers({
parent: deepCombinReducer(parentReducer, {
sub: subReducer
})
})
Ensuite, vous pouvez obtenir le magasin comme ceci:
{
parent: {
sub: []
}
}
dispatch({
type: 'ADD',
sub: '123'
});
// the store will change to:
{
parent: {
sub: ['123']
}
}
@smashercosmo immutable.js
avec un état imbriqué profond? Je suis curieux de savoir comment
@gaearon
Nous n'avons jamais d'entités à l'intérieur d'autres entités.
Je ne comprends pas. Nous avons au moins trois niveaux d'imbrication ici:
{
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]
}
entities.plans[1] - three levels
entities.exercises[1] - three levels
Il s'agit d'un objet non imbriqué. Un seul niveau.
{
plans: [1,2, 3],
exercises: [1,2,3],
'so forth': [1,2,3]
}
@wzup : Pour
La signification du terme «imbrication» ici est lorsque les données elles-mêmes sont imbriquées, comme dans l'exemple ci-dessus dans le thread:
{
plans: [
{title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
{title: 'B', exercises: [{title: 'exe5'}, {title: 'exe6'}]}
]
}
Dans cet exemple, la seule façon d'accéder à l'exercice "exe6" est de creuser dans la structure, comme plans[1].exercises[2]
.
Je suis intéressé par la question de @tmonte :
Comment allez-vous ajouter un exercice à un plan, en utilisant la même structure que l'exemple du monde réel? Disons que l'ajout d'un exercice a renvoyé l'entité d'exercice nouvellement créée, avec un champ planId. Est-il possible d'ajouter le nouvel exercice à ce plan sans avoir à écrire un réducteur pour les plans et d'écouter spécifiquement une action CREATE_EXERCISE?
Lorsque vous avez plusieurs entités, créer un réducteur pour chaque entité peut être douloureux, mais cette approche pourrait le résoudre. Je n'ai pas encore trouvé de solution pour cela.
J'utilise généralement mergeWith
au lieu de merge
pour plus de flexibilité:
import mergeWith from 'lodash/mergeWith';
// Updates an entity cache in response to any action with `entities`.
function entities(state = {}, action) {
// Here where we STORE or UPDATE one or many entities
// So check if the action contains the format we will manage
// wich is `payload.entities`
if (action.payload && action.payload.entities) {
// if the entity is already in the store replace
// it with the new one and do not merge. Why?
// Assuming we have this product in the store:
//
// products: {
// 1: {
// id: 1,
// name: 'Awesome product name',
// rateCategory: 1234
// }
// }
//
// We will updated with
// products: {
// 1: {
// id: 1,
// name: 'Awesome product name',
// }
// }
//
// The result if we were using `lodash/merge`
// notice the rate `rateCategory` hasn't changed:
// products: {
// 1: {
// id: 1,
// name: 'Awesome product name',
// rateCategory: 1234
// }
// }
// for this particular use case it's safer to use
// `lodash/mergeWith` and skip the merge
return mergeWith({}, state, action.payload.entities, (oldD, newD) => {
if (oldD && oldD.id && oldD.id === newD.id) {
return newD;
}
return undefined;
});
}
// Here you could register other handlers to manipulate
// the entities
switch (action.type) {
case ActionTypes.SOME_ACTION:
// do something;
default:
return state;
}
}
const rootReducer = combineReducers({
entities,
});
export default rootReducer;
Commentaire le plus utile
Oui, nous vous suggérons de normaliser vos données.
De cette façon, vous n'avez pas besoin d'aller «en profondeur»: toutes vos entités sont au même niveau.
Donc votre état ressemblerait à
Vos réducteurs pourraient ressembler à
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 des identifiants. Ainsi, 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 à la fois une entité appropriée dans le réducteurplans
_et_ en ajoutant son ID au réducteurcurrentPlans
. C'est important. Dans les applications plus complexes, vous pouvez avoir des relations, par exempleplans
reducer peut gérerADD_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 lui-même est mis à jour, _il n'y a pas besoin deplans
reducer pour le savoir, car l'ID n'a pas changé_.Troisièmement, notez que les réducteurs d'entités (
plans
etexercises
) ont des clauses spéciales pour surveilleraction.entities
. C'est au cas où nous aurions une réponse du serveur avec une «vérité connue» que nous voulons mettre à jour toutes nos entités pour refléter. 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 repo 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. C'est hors de portée de ma réponse - parfois vous voulez plus de flexibilité, et parfois vous voulez moins de passe-partout. Vous pouvez consulter le code de pagination dans des exemples de réducteurs «du monde réel» pour un exemple de génération de réducteurs similaires.
Oh, et j'ai utilisé la syntaxe
{ ...a, ...b }
. Il est activé dans Babel étape 2 en tant que proposition ES7. Il s'appelle «opérateur de propagation d'objet» et équivaut à écrireObject.assign({}, a, b)
.En ce qui concerne les bibliothèques, vous pouvez utiliser Lodash (faites attention à ne pas muter, par exemple
merge({}, a, b}
est correct maismerge(a, b)
ne l'est pas), updeep , -addons-update ou autre chose. Cependant, si vous avez besoin de faire des mises à jour approfondies, cela signifie probablement que votre arborescence d'états n'est pas assez plate et que vous n'utilisez pas suffisamment la composition fonctionnelle. Même votre premier exemple:peut être écrit comme