-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to cut the boilerplate when updating nested entities? #994
Comments
It's recommended to normalize nested JSON like: https://github.com/gaearon/normalizr |
Yes, we suggest normalizing your data. So your state would look like {
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]
} Your reducers might look like 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
}); So what's going on here? First, note that the state is normalized. We never have entities inside other entities. Instead, they refer to each other by IDs. So whenever some object changes, there is just a single place where it needs to be updated. Second, notice how we react to Third, notice that the entities reducers ( Finally, notice how entities reducers are similar. You might want to write a function to generate those. It's out of scope of my answer—sometimes you want more flexibility, and sometimes you want less boilerplate. You can check out pagination code in “real world” example reducers for an example of generating similar reducers. Oh, and I used As for libraries, you can use Lodash (be careful not to mutate though, e.g. 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)
]
}; can be written as 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)
}; |
It would be nice to turn this into a recipe. |
Ideally we want a kanban board example. |
@andre0799 or you could just use Immutable.js )) |
I wrote one. Perhaps you could fork it and tweak to your liking. |
Immutable.js is not always a good solution. It recalculates hash of every parent node of state starting from node you changed and that becomes bottleneck in particular cases (this is not very common cases tho). So ideally you should make some benchmarks before integrating Immutable.js into your application. |
Thanks @gaearon for your answer, great explanation! |
So, when you do |
While generally I'm in favor of many reducers handling the same action, it can get too complicated for entities with relationships. Indeed, I am suggesting to model these as separate actions. You can use Redux Thunk middleware to write an action creator that calls them both: 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
}
});
};
} Then, if you apply Redux Thunk middleware, you can call it normally: store.dispatch(createPlan(title)); |
So let's say I have a post editor on the backend somewhere with such relationships (posts, authors, tags, attachments, etc). How do I go about displaying Does all this belong in a reducer composition? I am missing something here... |
Regarding the original question, I believe React Immutability Helpers were created for that purpose |
This is correct. You'd do everything when retrieving the data. Please consult "shopping cart" and "real world" examples that come with Redux repo. |
Thanks, I already started to get an idea after reading Computing Derived Data on the docs. I will check those examples out again. I probably didn't understand a lot of what was going at first when I read them. |
Any this.props.dispatch(createPlan(title)); This is a usage question that is unrelated to this thread. It's better to consult examples or to create StackOverflow questions for this. |
I agree with Dan with normalizing the data, and flattening the state structure as much as possible. It may be put as a recipe / best practice in the documentation, as it would have saved me some headaches. As I made the mistake of having a bit of depth in my state, I made this lib to help retrofitting and managing deep state with Redux: https://github.com/baptistemanson/immutable-path |
This was helpful. Thanks to all. |
How would you go about adding an exercise to a plan, using the same structure as the real-world example? Let's say that adding an exercise returned the newly created exercise entity, with a |
Great discussion and information here. I would like to use normalizr for my project but have a question regarding to saving the updated data back to a remote server. Mainly, is there are simple way to revert the normalized shape back to the nested shape provided by the remote api after updating? This is important when the client makes changes and needs to feed it back to the remote api where he has no control over the shape of the update request. For example: client fetches the nested exercise data -> client normalizes it and stores it in redux -> user makes changes to the normalized data on client side -> user clicks save -> client app transforms the updated normalized data back to the nested form so it can submit it to the remote server -> client submits to server If I used normalizr would I need to write a custom transformer for the bolded step or is there a library or a helper method you'd recommend for this? Any recommendations would be much appreciated. thanks |
There’s something called https://github.com/gpbl/denormalizr but I’m not sure how closely it tracks normalizr updates. I wrote normalizr in a couple of hours for the app I worked on, you are welcome to fork it and add denormalization 😄 . |
Cool I definitely will take a look at denormalization and contribute back to your project once I have something. Great work for a couple of hours ;-) Thanks for getting back to me. |
I'm kind of in the same situation with changing deeply nested data, however, I've found a working solution using immutable.js. Is it alright if I link to a StackOverflow post I made asking for feedback on the solution here? I'm linking it for now, please do delete my post or say if it's inappropriate to link here: |
I've seen this approach being recommended in the past. However, this approach seems not to work well when a nested object needs to be removed. In this case, the reducer would need to look through all references to the object and remove them, an operation which would be O(n), before being able to remove the object itself. Anyone ran into a similar problem and solved it? |
@ariofrio : ah... I'm confused. The point of normalization is that the objects are not stored nested, and there's only one reference to a given item, making it easy to update that item. Now, if there's multiple other entities that "referenced" this item by ID, sure, they'd need to be updated as well, same as if things were not normalized. Is there a specific concern you have, or a troublesome scenario you're dealing with? |
Here is what I mean. Say the current state looks like:
In this example, each exercise can only be referenced by one plan. When the user clicks on "Remove Exercise", the message might look something like this:
But to implement this properly, one would need to iterate over all plans, and then all exercises within each plan, to find the one that referenced the exercise with id 2, in order to avoid a dangling reference. This is the O(n) operation I was worried about. A way to avoid this is to include the plan id in the payload of REMOVE_EXERCISE, but at this point, I don't see the advantage over using nesting the structures. If we used nested state instead, the state might look like:
And the message to remove the exercise might look like:
|
A few thoughts:
Ultimately, both nested and normalized state are conventions. There's good reasons to use normalized state with Redux, and there may be good reasons to keep your state normalized. Pick whatever works for you :) |
my solution like this:
Then, you can get the store like this:
|
@smashercosmo |
I don't understand it. We have at least three levels of nesting here:
This is a not nested object. Only one level.
|
@wzup : FYI, Dan doesn't spend much time looking at Redux issues these days - he's got enough on his plate working on React. The meaning of "nesting" here is when the data itself is nested, like this example from earlier in the thread: {
plans: [
{title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
{title: 'B', exercises: [{title: 'exe5'}, {title: 'exe6'}]}
]
} In that example, the only way to access exercise "exe6" is to dig into the structure, like |
I'm interested in @tmonte 's question:
When you have many entities, creating one reducer for each entity could be painful, but this approach could solve it. I haven't found a solution for it so far. |
I presonnaly use 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; |
So I have this nested structure under my state
I'm trying to create a reduce which does not mutate the previous state, but it's getting to a point where I'm spending more time figuring out how to do this then coding the rest of the app, it's getting pretty frustrating.
For example, if I wanted to add a new empty exercise or update an existing one, mutating the data I would just do:
but what could be my best approach to do the same in this nested structure? I've read the Redux docs and also the troubleshooting part, but the furthest I got was updating a plan, where I would do:
Isn't there a faster way to work with this? Even if I have to use external libraries, or at least if someone can explain me how to better deal with this...
Thank you!
The text was updated successfully, but these errors were encountered: