React -Update a nested object in redux state of an unknown depth - javascript

When a user loads a blog post page, a post is fetched and its comments and subcomments are stored in the Redux state with the following structure:
forumPost: {
id: #,
data: {object},
comments: [
{
id: #,
data: {object},
comments: [
{
id: #,
data: {object},
comments: [...]
},
...
]
},{
id: #,
data: {object},
comments: [...]
},
...
}
When a user posts a new comment/subcomment, it is posted to the database and locally added to the Redux state. I am having problems with the second part
I created a function for finding the path to the parent comment of the comment being posted but am having trouble injecting the new comment into the current state.
const findPath = (comments, parentId) => {
let path = [];
const findIndex = (arry, count) => {
for (; count < arry.length; count++) {
if (arry[count].id === parentId) {
path = "forumPost.comments[" + count + "].comments";
return true;
}
if (arry[count].comments && arry[count].comments.length > 0) {
if (findIndex(arry[count].comments, 0)) {
path = path
.split("forumPost")
.join("forumPost.comments[" + count + "]");
return true;
}
}
}
};
findIndex(comments, 0);
return path; //Returns the parent's path as 'forumPost.comments[a].comments[b].comments'
};
I can create an entirely new state, add the new comment, and replace the old state, but doing so is expensive and (as I understand it) makes it impossible for a component to tell if the Redux state (mapped to props) has changed.
So, having done some research I have come to understand that redux treats updating and replacing the state differently and what I want to do is update the state.
https://reactjs.org/docs/update.html
Cleaner/shorter way to update nested state in Redux?
Object spread vs. Object.assign
I’ve tried to use .update(), …spreads, and dot-prop, but haven’t been able to get them to work (most likely because I have been using them wrong)… and that’s why I am here. How do you update the state with a new comment object to one of the comments arrays of an unknown depth?
Also, I now know that its best practice to keep your redux state as shallow as possible. But the current structure is what it is.

Related

Is it a bad practice to set property of object, nested in another object by directly setting it on destructured copy of state?

I've found an article which states that if i want to change property name in such state:
const [user, setUser] = useState({
name: 'Cody',
age: 25,
education: {
school: {
name: 'School of Code'
}
}
})
i need to do following:
setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
})
Howewer, they later show that it is possible to make this logic simpler, using immer.js (also changing useState on useImmer), like this:
setUser(draft => {
draft.education.school.name = 'Layercode Academy';
})
My question is whether i can do this, without using immer.js:
setUser(prevUser => {
const newUser = {...prevUser}
newUser.education.school.name = 'Layercode Academy'
return newUser
})
In every tutorial i've seen (that doesn't use immer.js), they do destructuring. But just assigning value to property of state copy seems simpler and more concise for me in many situations. I am not setting state directly, but rather just modify copy, which is not breaking any "rules" . Are there some hidden pitfalls?
Here is the difference between the copies
SHALLOW COPY
const a = { x: 0, y: { z: 0 } };
const b = {...a}; // or const b = Object.assign({}, a);
b.x = 1; // doesn't update a.x
b.y.z = 1; // also updates a.y.z
DEEP COPY
const a = { x: 0, y: { z: 0 } };
const b = JSON.parse(JSON.stringify(a));
b.y.z = 1; // doesn't update a.y.z
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. - Dan
So with this in mind, you don't have to deep clone all things as it can be very expensive and cause unnecessary renders.
so if you have to do something with the data after the render
setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
})
doSomething(user) // setUser merely schedules a state change, so your 'user' state may still have old value
}
Instead,
setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
}, (user) => { doSomething(user})
This way you guarantee the update of the user and don't have to worry about the mutation.
Are there some hidden pitfalls?
Yes, though we can't know if the rest of your code will experience them. So in general just consider it as a blanket "yes".
What you're doing is mutating state. In cases where the entire operation is just performing one state update and re-rendering, you're unlikely to experience a problem. But it's still building upon and relying upon a bad habit.
In cases where more operations are being performed (multiple batched state updates, other logic using current state before state updates are processed, etc.) then you're much more likely to see unexpected (and difficult to trace) bugs and behaviors.
The overall rule of "don't mutate state" is just that, an overall rule. There are indeed cases where one can mutate state without causing any problems. But why? Why rely on bad habits solely because a problem hasn't occurred yet?
A state update should be a wholly new object (or array, or just plain value). For object properties which don't change, it can still be a reference to the previous object property (which deconstructing will do). But for any property which does change, the entire object graph which leads to that property should be new.
Yep, there is something wrong with your last example. You're still modifying the original user's education object:
setUser(prevUser => {
const newUser = {...prevUser}
newUser.education.school.name = 'Layercode Academy'
// true
prevUser.education.school.name === newUser.education.school.name
return newUser;
});
You may consider breaking up state to multiple states (that are primitives):
const [userEducationSchoolName, setUserEducationSchoolName] = useState("School of Code");
but this gets out of hand very quickly. If your environment supports it, you may even use structuredClone:
setUser(prevUser => {
const newUser = structuredClone(prevUser);
newUser.education.school.name = 'Layercode Academy'
return newUser
});
but since some browsers don't support structuredClone yet, you would need some other way to deep clone an object. And this brings us back to Immer! The whole point of Immer is to give you a mutable deep copy of the state for you to change.

What do you suggest to make undo?

I use a series of data as follows:
[
{
name: 'name1',
background:'red',
child:[
{
name:'',
id:'',
color:'',
text:'',
border:''
},
{
name:'',
id:'',
color:'',
text:'',
border:''
}
]
},
{
name: 'name2',
background:'red',
child:[
{
name:'',
id:'',
color:'',
text:'',
border:''
},
{
name:'',
id:'',
color:'',
text:'',
border:''
}
]
}
]
I'm going to save all the changes to another variable, and I used a deep copy to do that, but when I log in, the variables are the same.I need to children all the children changes too.
I wrote it in Reducers
const Reducers =(state = initialState, action) => {
switch (action.type) {
case NEW_OBJECTS_PAST:
const OldPast = JSON.parse(JSON.stringify(state.past))
const newDate = JSON.parse(JSON.stringify(state.present))
// const newDate = _.cloneDeep(state.present);
const newPast = [
OldPast,
newDate
];
return {
...state,
past : _.cloneDeep(newPast) ,
}
case OBJECTS:
return {
...state,
present: action.objects,
// future:[]
}
Do you suggest another way to build undo in REACT and REDUX ?
I tried the libraries available for this, but got no answer.
First two remarks :
you should never deep clone parts of your state, it doesn't bring you any benefits as the state is immutable anyway, but is very detrimental for memory usage and for performance (both when doing the deep cloning and then when using the deep cloned state),
you should use Redux toolkit, which makes it way easier to write immutable reducers and prevent many errors.
To implement undo, I'm not sure what your actions are supposed to mean but you can do it as follows
the state contains state.present (the current state) and state.past (an array of past states)
whenever you do an action that you want to undo, you push the current state.present at the end of state.past and compute the new state.present
whenever you want to undo, you pop the last element of state.past and put it in state.present.
In your code I can't see any undo action, and you're also building nested arrays because of new Past = [oldPast, new Date], you most likely meant to spread oldPast.

Zustand: change value of parameter in stored object

I am trying to create a function for a state of rated movies in Zustand.
The state consists of an array of objects, example with two entries:
ratedMovies: [
{ imdbID: "tt0076759", userRating: "5" },
{ imdbID: "tt0080684", userRating: "10" },
]
Below is the function managing ratedMovies changes. Here is where the issue lies. I want it to check whether an object with the same imdbID is present in ratedMovies state. And if so to update the value of it, instead of adding another object with the same imdbID but a new value.
If I try to change the rating of one of the movies from the above state example (with them in the state ofc), I get the IF console check and the app crashes with the error:
TypeError: Cannot create property 'userRating' on number '0'
If the state is empty or I change the rating of other movies, I get the ELSE console check, but they are still not added into the state.
addUserRating: (rating) => {
console.log("rating object", rating)
set((state) => {
if (state.ratedMovies.find((movie) => movie.imdbID === rating.imdbID)) {
console.log("add rating IF")
let index = state.ratedMovies.findIndex(
(movie) => movie.imdbID === rating.imdbID
)
index.userRating = rating.userRating
return [index, ...state.ratedMovies]
} else {
console.log("add rating ELSE")
return [rating, ...state.ratedMovies]
}
})
}
the onChange function on the input where one can rate a movie creates an identical object as in the state array and passes it to the function managing the state of ratedMovies:
const changeUserMovieRating = (event) => {
const movieRating = {
imdbID: modalDetails.imdbID,
userRating: event.target.value,
}
console.log(movieRating)
addUserRating(movieRating)
}
Output of which is:
{imdbID: 'tt0120915', userRating: '2'}
I hope i explained everything clearly, and I will highly appreciate any tips on how to solve this issue, thanks in advance!
Sorry but this whole apprach I had at the time of asking this question had no sense and had to be reworked completely.
I decided not to add the parameter during the fetch, as in another component the same data could be fetched. So I decided to instead keep the value of the 'userRating' in the local storage and if the fetched movie was already once rated by the 'user', the value would be displayed.

React set state to nested object value

I need to set state on nested object value that changes dynamically Im not sure how this can be done, this is what Ive tried.
const [userRoles] = useState(null);
const { isLoading, user, error } = useAuth0();
useEffect(() => {
console.log(user);
// const i = Object.values(user).map(value => value.roles);
// ^ this line gives me an react error boundary error
}, [user]);
// This is the provider
<UserProvider
id="1"
email={user?.email}
roles={userRoles}
>
The user object looks like this:
{
name: "GGG",
"website.com": {
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
I need to grab the roles value but its parent, "website.com" changes everytime I call the api so i need to find a way to search for the roles.
I think you need to modify the shape of your object. I find it strange that some keys seem to be fixed, but one seems to be variable. Dynamic keys can be very useful, but this doesn't seem like the right place to use them. I suggest that you change the shape of the user object to something like this:
{
name: "GGG",
site: {
url: "website.com",
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
In your particular use case, fixed keys will save you lots and lots of headaches.
You can search the values for an element with key roles, and if found, return the roles value, otherwise undefined will be returned.
Object.values(user).find(el => el.roles)?.roles;
Note: I totally agree with others that you should seek to normalize your data to not use any dynamically generated property keys.
const user1 = {
name: "GGG",
"website.com": {
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
const user2 = {
name: "GGG",
friends: {},
otherData: {}
}
const roles1 = Object.values(user1).find(el => el.roles)?.roles;
const roles2 = Object.values(user2).find(el => el.roles)?.roles;
console.log(roles1); // ["SuperUser"]
console.log(roles2); // undefined
I would recommend what others have said about not having a dynamic key in your data object.
For updating complex object states I know if you are using React Hooks you can use the spread operator and basically clone the state and update it with an updated version. React hooks: How do I update state on a nested object with useState()?

Trouble triggering reactivity on changed Vuex objects

I'm modifying the value of an existing property on an object that is in an array of objects in my Vuex.store. When I update the store, it is not triggering a re-render of my computed property that is accessing the store. If I reset the stored value to an empty array, and then set it again to my new array, it'll trigger the change. But simply updating the property of the array of objects does not trigger a change.
I have tried using Vue.set() like the docs talk about, and that updates the store, but still does not trigger a re-render of the computed property. What am I missing? Using Vue 2.2.4 and Vuex 2.2.0.
//DEBUG: An example of the updated post I'm adding
let myNewScheduledPost = {
id: 1,
name: 'James'
};
this.$store.dispatch('addScheduledPost', post);
//DEBUG: My store
const options = {
state: {
scheduledPosts: [
{ id: 1, name: 'Jimmy'}
],
},
mutations: {
scheduledPosts: (state, scheduledPosts) => {
//This triggers the reactivity/change so my computed property re-renders
//But of course seems the wrong way to do it.
state.scheduledPosts = [];
state.scheduledPosts = scheduledPosts;
//Neither of these two lines triggers my computed property to re-render, even though there is a change in scheduledPosts
state.scheduledPosts = scheduledPosts;
Vue.set(state, 'scheduledPosts', scheduledPosts);
},
},
actions: {
addScheduledPost({ commit, getters }, newScheduledPost) {
let scheduledPosts = getters.scheduledPosts;
const idx = scheduledPosts.findIndex(existingScheduledPost => existingScheduledPost.id === newScheduledPost.id);
//If the post is already in our list, update that post
if (idx > -1) {
scheduledPosts[idx] = newScheduledPost;
} else {
//Otherwise, create a new one
scheduledPosts.push(newScheduledPost);
}
commit('scheduledPosts', scheduledPosts);
//DEBUG: This DOES have the correct updated change - but my component does not see the change/reactivity.
console.log(getters.scheduledPosts);
}
},
getters: {
scheduledPosts: (state) => {
return state.scheduledPosts;
}
}
};
//DEBUG: Inside of my component
computed: {
mySortedPosts()
{
console.log('im being re-rendered!');
return this.$store.getters.scheduledPosts.sort(function() {
//my sorted function
});
}
}
Your problem is if you are wanting to access a portion of the state you don't use a getter https://vuex.vuejs.org/en/state.html.
computed: {
mySortedPosts(){
return this.$store.state.scheduledPosts
}
}
Getters are for computed properties in the store https://vuex.vuejs.org/en/getters.html. So in your case you might create a getter to sort your scheduled posts then name it sortedScheduledPosts and then you can add it to your components computed properties like you are now.
The key thing is your getter needs to have a different name then your state property just like you would in a component.

Categories