Deep merge of complex state in React - javascript

When I have the following initial state declared:
getInitialState: function() {
return {
isValid: false,
metaData: {
age: 12,
content_type: 'short_url'
}
};
},
and I update state with setState like this:
...
let newMetaData = { age: 20 };
...
this.setState({
isValid: true,
metaData: newMetaData
});
...
Resulting this.state.metadata object has only age defined. But as far as I'm aware, this.setState() merges it argument to existing state. Why it's not working here, isn't this supposed to be recurrent merging?
Is there a way to merge new object properties to state object property in React/ES6?

setState performs a shallow merge. If metaData is is flat:
this.setState({
metaData: Object.assign({}, this.state.metaData, newMetaData),
});
or if using spread :
this.setState({
metaData: { ...this.state.metaData, ...newMetaData },
});

Another way to approach this, if you only need to update one property, would be like this:
this.setState({
metaData: {
...this.state.metaData,
age: 20
}
})

setState can also take a function, which receives an argument of state, and you can use lodash merge to do the deep merge.
setState(state => merge(state, yourPartialObjectToBeDeepMerged));

a tricky solution is here
const [complexObject, setComplexObject] = useState({a:{b:{c:1}}})
setComplexObject((s)=>{
s.a.b.c = 2
return {...s}
})
How does it work?
An object is a reference. If you s.a.b.c = 2, this will update the state, but the component doesn't rerender, so the dom will not change.
Then we need a way to rerender the component. If you setComplexObject(s=>s), it will not trigger rerender, because albeit the interior of the ComplexObject do change, the reference still point at the same object.
So we need ES6 spread operator to reconstruct a object by doing this {...s}

Related

Problems achieving required result of using the spread (...) operator with state object

I have a pimRegistration state initialization as shown in the chrome redux-devtools screen capture below. The nesting being referenced is pimRegistration (state.domain.patient):
I updated the patient.name object with the following spread operator statement:
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
...where property is the "name" property of the patient object with value. After the update, the following screenshot shows the new state:
Note that the original patient object (purple in the screenshot) is updated with the name object, duplicated and placed at the root of the state (yellow in screenshot).
I would like to overwrite the properties of the pimRegistration(state).domain.patient object, not to create a new patient object.
The state update is called as shown below.
store.update((state) => ({
...state,
...patientPath, // state.domain.patient
...{ [property]: value },
}));
I have tried my different combinations without achieving the desired result.
The complete update function is shown below.
update(property: string, path: string, value: any) {
const paths: string[] = path.split(".");
const pathReducer = (state: IRegistrationState, path_: string) => {
if (paths.length <= 0) {
return state.domain;
}
return state[path_];
};
const domainPath = state.domain;
let patientPath, nokPath, referrerPath;
if (path.includes("patient")) {
patientPath = paths.reduce(pathReducer, state);
}
if (path.includes("nok")) {
nokPath = paths.reduce(pathReducer, state);
}
if (path.includes("referrer")) {
referrerPath = paths.reduce(pathReducer, state);
}
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
}
The function above is invoked with the following statement in Angular 2.
if (this.path.includes("patient")) {
this._repo.update("name", "domain.patient", this.name);
}
Thanks
Deep updates to a store can be tricky. In your function you seem to be spreading the updates at the root rather than at the level you want the update at. This answer here outlines the usual practice to update the state. In short, something like
const newState = {
...state,
domain: {
...state.domain,
patient: {
...state.domain.patient,
[property]: value
}
}
}
Dynamically passing a path and updating this state can be… cumbersome. There are libraries that can help you do it such as immer, but you can possibly hack your way around with normal JS/TS.

Immutability and updating nested key of local state

I have the following state.
state = {
friends: {
nickNames: ['Polly', 'P', 'Pau'],
... here more k:v
},
}
}
And I want to update nickNames array with a value coming from an uncontrolled form through a method in my class. However, I'm having issues at the time of determine if I am setting the state properly without mutating it.
I am doing the following
updateArray = (nickName) => {
const tempDeepCopy = {
...this.state,
friends: {
...this.state.friends,
nickNames: [...this.state.friends.nickNames]
}
}
tempDeepCopy.friends.nickNames.push(nickName)
this.setState({
friends:
{
nickNames: tempDeepCopy.friends.nickNames
}
})
}
Is this the proper way of doing it? If so, is it also the most efficient given the state? I am trying to avoid helper libraries to learn how to make deep copies.
I will appreciate help since Im trying to learn immutability and it is a concept that is taking me a lot of effort.
setState() in class components does shallow merge.
So you could just ignore other "parent" keys and focus only on friends.
You can also simplify it like:
this.setState({
friends: { // only focus on friends
...this.state.friends, // do not ignore other friend k:v pairs
nicknames: [
...this.state.friends.nicknames,
nickName
]
}
})
Why not just;
updateArray = (nickName) => {
const updatedNickNames = [...this.state.friends.nickNames, nickName];
this.setState({
friends: {
...this.state.friends,
nickNames: updatedNickNames
}
});
}
Because nickNames is just a array of strings, you can copy it with the spread operator. Also, with setState you can change a specific part of your state, in your case you only have to worry about the friends part.
You can just use
this.setState({
friends:
{
nickNames: [...this.state.friends.nickNames, nickName]
}
})
State should be only modified through the setState function because if you modify it directly you could break the React component lyfecycle.
It is a correct way to do that. The thing is you must never change the state directly. That is it.
I might give a more concise code
state = {
friends: {
nickNames: ['Polly', 'P', 'Pau'],
... here more k:v
},
}
}
updateArray = (nickName) => {
this.setState(prevState => ({
...prevState,
friends:
{
...prevState.friends,
nickNames: [...prevState.friends.nickNames, nickname]
}
}))
}
As long as you do not change the state directly, any way will do through setState()

What's the best alternative to update nested React state property with setState()?

I've been thinking about what would be the best way among these options to update a nested property using React setState() method. I'm also opened to more efficient methods considering performance and avoiding possible conflicts with other possible concurrent state changes.
Note: I'm using a class component that extends React.Component. If you're using React.PureComponent you must be extra careful when updating nested properties because that might not trigger a re-render if you don't change any top-level property of your state. Here's a sandbox illustrating this issue:
CodeSandbox - Component vs PureComponent and nested state changes
Back to this question - My concern here is about performance and possible conflicts between other concurrent setState() calls when updating a nested property on state:
Example:
Let's say I'm building a form component and I will initialize my form state with the following object:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
From my research, by using setState(), React will shallow merge the object that we pass to it, which means that it's only going to check the top level properties, which in this example are isSubmitting and inputs.
So we can either pass it a full newState object containing those two top-level properties (isSubmitting and inputs), or we can pass one of those properties and that will be shallow merged into the previous state.
QUESTION 1
Do you agree that it is best practice to pass only the state top-level property that we are updating? For example, if we are not updating the isSubmitting property, we should avoid passing it to setState() in other to avoid possible conflicts/overwrites with other concurrent calls to setState() that might have been queued together with this one? Is this correct?
In this example, we would pass an object with only the inputs property. That would avoid conflict/overwrite with another setState() that might be trying to update the isSubmitting property.
QUESTION 2
What is the best way, performance-wise, to copy the current state to change its nested properties?
In this case, imagine that I want to set state.inputs.username.touched = true.
Even though you could do this:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
You shouldn't. Because, from React Docs, we have that:
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props.
So, from the excerpt above we can infer that we should build a new object from the current state object, in order to change it and manipulate it as we want and pass it to setState() to update the state.
And since we are dealing with nested objects, we need a way to deep copy the object, and assuming you don't want to use any 3rd party libraries (lodash) to do so, what I've come up with was:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
Note that when your state has nested object you also shouldn't use let newState = Object.assign({},state). Because that would shallow copy the state nested object reference and thus you would still be mutating state directly, since newState.inputs === state.inputs === this.state.inputs would be true. All of them would point to the same object inputs.
But since JSON.parse(JSON.stringify(obj)) has its performance limitations and also there are some data types, or circular data, that might not be JSON-friendly, what other approach would you recommend to deep copy the nested object in order to update it?
The other solution I've come up with is the following:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
What I did in this second alternative was to create an new object from the innermost object that I'm going to update (which in this case is the username object). And I have to get those values inside the key username, and that's why I'm using usernameInput['username'] because later I will merge it into a newInputs object. Everything is done using Object.assign().
This second option has gotten better performance results. At least 50% better.
Any other ideas on this subject? Sorry for the long question but I think it illustrates the problem well.
EDIT: Solution I've adopted from answers below:
My TextInput component onChange event listener (I'm serving it through React Context):
onChange={this.context.onChange(this.props.name)}
My onChange function inside my Form Component
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
I can think of a few other ways to achieve it.
Deconstructing every nested element and only overriding the right one :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
Using the deconstructing operator to copy your inputs :
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
EDIT
First solution using computed properties :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs.[field],
[action]: value
}
}
}))
You can try with nested Object.Assign:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
You can also use spread operator:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
This is proper way to update nested property and keep state immutable.
I made a util function that updates nested states with dynamic keys.
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
You pass a state along with a list of key selectors and the new value.
You can also set the autoAssign value to false to return an object that is a copy of the old state but with the new updated field - otherwise autoAssign = true with update the previous state.
Lastly, if the sequence of selectors don't appear in the object, an object and all nested objects with those keys will be created.
Use the spread operator
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})

Vuex: How to store the result of a getter in the state?

I am having a problem in my Vuex.Store:
I would like to get an object (getter.getRecipe) using two state entries as search criteria (state.array & state.selected) with a getter. Then I would like to store that result in my state (state.recipe) in order to be able to update it within components (i.e., changing one key of the recipe object based on client action).
However, I have no idea how I can store the result of getters in my state ("this.getters.getRecipe" is not working...).
Very helpful for hints. Thanks a lot.
//store.js (vuex store)
export const store = new Vuex.Store({
state: {
array: [recipe1, recipe2],
selected: 0,
recipe: this.getters.getRecipe()
},
getters: {
getRecipe: (state) => {
return state.array[state.selected]
}
}
})
Instead, you should use Method style getters with arguments:
You can also pass arguments to getters by returning a function. This is particularly useful when you want to query an array in the store:
A basic example:
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
Then, to use the example:
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
or, from inside a component:
this.$store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
This way you ensure your state remains a pure data structure that follows the one way data flow set forth by the flux pattern.

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