Immutability and updating nested key of local state - javascript

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()

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.

Add elements to a list that belongs to a state using react

I have this json variable on my state:
this.state = {
type:
{
name: '',
type2: {
atribute: '',
parameter: [
{
value: '',
classtype: ''
}
],
name: '',
atribute1: '',
atribute2: ''
}
}
}
what I wanted to do is to add elements to my parameter list,which is empty on the beggining.
What I did was this:
addParams = () => {
let newParam = {
value: this.state.type.type2.parameter.value,
classtype: this.state.type.type2.parameter.classtype
};
/** */
this.setState(prevState => ({
type: {
// keep all the other key-value pairs of type
...prevState.type,
type2: {
...prevState.type.type2,
//this is supposed to add an element to a list
parameter: [...prevState.type.type2.parameter, newParam]
}
}
}))
}
But when executing the last line of code the following error appeared:
Uncaught TypeError: Invalid attempt to spread non-iterable instance
when spreading a list
I do not know why this does not work because the parameter is a list indeed.
If this.state.type.type2.parameter is an array then why are you referencing properties on it:
let newParam = {
value: this.state.type.type2.parameter.value,
classtype: this.state.type.type2.parameter.classtype
};
I don't think your state is structured how you expect, seems like you're replacing that array with an object at some point in your code. I suggest react-devtools to help you keep track of your state as it changes.
This isn't exactly an answer but I highly suggest using immerjs for doing these pure nested updates. I almost never recommend adding a third party library as a solution but immer is lightweight and life changing. It exports a single function called produce and uses a concept called a Proxy to perform pure updates that are written as mutations.
With immer (and your bug fixed) your code becomes this:
const newParam = {
value: this.state.type.type2.parameter.value,
classtype: this.state.type.type2.parameter.classtype
};
this.setState(produce(draftState => {
draftState.type.type2.parameter.push(newParam);
}))
It lets you write more terse code that is a lot easier to read. And yes I know that looks like a mutation, but it isn't one this is 100% pure.
Try this :
this.setState(prevState => {
return {
...prevState,
type: {
...prevState.type,
type2: {
...prevState.type.type2,
parameter: [...prevState.type.type2.parameter, newParam]
}
}
}
)

How do I update the value of an object property inside of an array in React state

I cannot seem to find an answer on here that is relevant to this scenario.
I have my state in my React component:
this.state = {
clubs: [
{
teamId: null,
teamName: null,
teamCrest: null,
gamesPlayed: []
}
]
}
I receive some data through API request and I update only some of the state, like this:
this.setState((currentState) => {
return {
clubs: currentState.clubs.concat([{
teamId: team.id,
teamName: team.shortName,
teamCrest: team.crestUrl
}]),
}
});
Later on I want to modify the state value of one of the properties values - the gamesPlayed value.
How do I go about doing this?
If I apply the same method as above it just adds extra objects in to the array, I can't seem to target that specific objects property.
I am aiming to maintain the objects in the clubs array, but modify the gamesPlayed property.
Essentially I want to do something like:
clubs: currentState.clubs[ index ].gamesPlayed = 'something';
But this doesn't work and I am not sure why.
Cus you are using concat() function which add new item in array.
You can use findIndex to find the index in the array of the objects and replace it as required:
Solution:
this.setState((currentState) => {
var foundIndex = currentState.clubs.findIndex(x => x.id == team.id);
currentState.clubs[foundIndex] = team;
return clubs: currentState.clubs
});
I would change how your state is structured. As teamId is unique in the array, I would change it to an object.
clubs = {
teamId: {
teamName,
teamCrest,
gamesPlayed
}
}
You can then update your state like this:
addClub(team) {
this.setState(prevState => ({
clubs: {
[team.id]: {
teamName: team.shortName,
teamCrest: teamCrestUrl
},
...prevState.clubs
}
}));
}
updateClub(teamId, gamesPlayed) {
this.setState(prevState => ({
clubs: {
[teamId]: {
...prevState.clubs[teamId],
gamesPlayed: gamesPlayed
},
...prevState.clubs
}
}));
}
This avoids having to find through the array for the team. You can just select it from the object.
You can convert it back into an array as needed, like this:
Object.keys(clubs).map(key => ({
teamId: key,
...teams[key]
}))
The way I approach this is JSON.parse && JSON.stringify to make a deep copy of the part of state I want to change, make the changes with that copy and update the state from there.
The only drawback from using JSON is that you do not copy functions and references, keep that in mind.
For your example, to modify the gamesPlayed property:
let newClubs = JSON.parse(JSON.stringify(this.state.clubs))
newClubs.find(x => x.id === team.id).gamesPlayed.concat([gamesPlayedData])
this.setState({clubs: newClubs})
I am assuming you want to append new gamesPlayedData each time from your API where you are given a team.id along with that data.

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
})

Deep merge of complex state in React

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}

Categories