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.
Related
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.
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()
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]
}
}
}
)
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
})
I have an app which has a few users. I would now like to be able to create a new user. So I have created this actionCreator:
export const createUser = (first, last) => {
console.log("You are about to create user: XX ");
return {
type: 'USER_CREATE',
first: first,
last: last,
payload: null
}
};
I am dealing only with first & last names for now. The actionCreator gets its parameters from the container. There is a button which calls the actionCreator like so:
<button onClick={() =>this.props.createUser(this.state.inputTextFirstName, this.state.inputTextLastName)}>Submit</button>
My UserReducer looks like this:
/*
* The users reducer will always return an array of users no matter what
* You need to return something, so if there are no users then just return an empty array
* */
export default function (state = null, action) {
if(state==null)
{
state = [
{
id: 1,
first: "Bucky",
last: "Roberts",
age: 71,
description: "Bucky is a React developer and YouTuber",
thumbnail: "http://i.imgur.com/7yUvePI.jpg"
},
{
id: 2,
first: "Joby",
last: "Wasilenko",
age: 27,
description: "Joby loves the Packers, cheese, and turtles.",
thumbnail: "http://i.imgur.com/52xRlm8.png"
},
{
id: 3,
first: "Madison",
last: "Williams",
age: 24,
description: "Madi likes her dog but it is really annoying.",
thumbnail: "http://i.imgur.com/4EMtxHB.png"
}
]
}
switch (action.type) {
case 'USER_DELETED':
return state.filter(user => user.id !== action.userIdToDelete);
case 'USER_CREATE':
console.log("Action first:" + action.first);
console.log("Action last:" + action.last);
Object.assign({}, state, {
id: 4,
first: action.first,
last: action.last,
age: 24,
description: "Some new Text",
thumbnail: "http://i.imgur.com/4EMtxHB.png"
});
return state;
}
return state;
}
Now I have a few questions.
1) Is this the proper way to do this, or am I writing bad code somewhere? Keep in mind that I am trying to use Redux here, I am not entirely sure though whether I am not sometimes falling back into React without Redux
2) Am I doing the state thing correctly? I initially used a tutorial and am now building upon that, but I am not sure why state seems to be an array:
state = [ <--- Does this mean that my state is an array?
{
id: 1,
// and so on ...
I am very confused by this, since in other Tutorials state is just an object containing other smaller objects and its all done with parentheses { }
3) What would be the best way to create a new user. My Object.assign does not work, it does not update anything, and I am not sure where the mistake lies.
4) And, relatedly, how could I update one individual user or a property of one individual user?
As Flyer53 states you need to set the state to the return value of Object.assign() as this is designed to not mutate state it will not change the value of the state you're passing in.
The code's fine; I'd tend to use just one property on the action in addition to its type, so have a property of (say) user that is an object containing all the user data (first name, last name etc).
I believe it's quite idiomatic to define a default state outside of the reducer and then set this as the default value for the state parameter in the reducer function:
export default function (state = initialState, action) {
For a brilliant introduction by its creator, see https://egghead.io/courses/getting-started-with-redux
State can be any shape you like. As an application grows in complexity it will usually be represented as an object composed of different sections of data. So, for example, in your case in could be comprised of an array of users and, say, an 'order by' that could apply to some UI state):
{ users: [], orderBy: 'lastName' }
If you carry on using an array of users as the state then you can use the ES6 spread operator to append the new user, for example:
newState = [ ...state, action.user ];
whereas if you move to using an object for state, the following would similarly append a user:
newState = Object.assign({}, state, { users: [ ...state.users, action.user ] };
Finally, to update a single user you could just use map against the array of users as follows (this is obviously hardcoded, but you could match, say, on id and update the appropriate properties).
let modifiedUsers = state.users.map((user) => {
if (user.id === 3) {
user.name = user.name + '*';
}
return user;
});
let newState = Object.assign({}, state, { users: modifiedUsers });
There's maybe an easier way to log the state(users) in an object (not in an array as in your example code above) that works without Object.assign() which is supposed to work with objects, not arrays:
var state = {
user1: {
id: 1,
first: "Bucky",
last: "Roberts",
age: 71,
description: "Bucky is a React developer and YouTuber",
thumbnail: "http://i.imgur.com/7yUvePI.jpg"
}
};
state['user' + 2] = {
id: 2,
first: "Joby",
last: "Wasilenko",
age: 27,
description: "Joby loves the Packers, cheese, and turtles.",
thumbnail: "http://i.imgur.com/52xRlm8.png"
};
console.log(state);
console.log(state.user2);
Just an idea ...