I've got a jsbin for this issue here: http://jsbin.com/tekuluve/1/edit
In an onClick event I'm removing an element from the model, and re-rendering the app. But strangely, in componentWillReceiveProps() (and componentWillUpdate, and componentDidUpdate too), nextProps is always === to this.props, regardless of what I do.
/** #jsx React.DOM */
var Box = React.createClass({
render: function() {
return (
<div className="box" onClick={ UpdateModel }>
{ this.props.label }
</div>
);
}
});
var Grid = React.createClass({
componentWillReceiveProps: function(nextProps) {
// WTF is going on here???
console.log(nextProps.boxes === this.props.boxes)
},
render: function() {
var boxes = _.map(this.props.boxes, function(d) {
return (<Box label={ d.number } />);
});
return (
<div className="grid">
{ boxes }
</div>
);
}
});
var model = [
{ number: 1 },
{ number: 2 },
{ number: 3 },
{ number: 4 },
{ number: 5 }
];
function UpdateModel() {
React.renderComponent(
<Grid boxes={ _.pull(model, _.sample(model)) } />,
document.body
);
}
React.renderComponent(
<Grid boxes={ model } />,
document.body
);
I need nextProps to be different to this.props after it has been updated via UpdateModel(), in the componentWillReceiveProps() lifecycle event.
Something like this occurred for me using Flux Stores to store my state in the manner suggested by the official Todo List tutorial (http://facebook.github.io/flux/docs/todo-list.html). For anyone else finding this after following the tutorial, a problem seems to arise in the TodoStore in the getAll() method, because it returns a direct reference to the internal data object:
getAll: function() {
return _todos;
}
This seems to breaks the ability of the lifecycle methods like componentDidUpdate(prevProps) to distinguish between the old and new props. I think the reason is that by passing a direct reference to the Store's data object into the views, the state/props effectively change immediately when the Store changes rather than after new values are passed down through the lifecycle methods, so old and new props always contain the same values. This could be solved by passing a copy of the internal data object _todos rather than the object itself,
for example, when _todos is an object,
getAll: function() {
return JSON.parse(JSON.stringify(_todos));
}
If _todos is an array, return _todos.slice() can be used instead.
In general, if using a Store to contain state information and call it from your controller-views, it seems advisable to return a copy of the data rather than a reference to the original, since the original will be mutated when state changes occur.
=== will check if it's the same object. It seems that what you're doing is mutating the value that the boxes property points to.
You could use Immutable.js. It is a library that Facebook made to work with react/flux.
https://facebook.github.io/immutable-js/
We have been stumped by this a couple of times.
shouldComponentUpdate(nextProps) {
return this.props.myObj !== nextProps.myObj
}
Many times, some value inside the myObj object would change. However the above function would return false. The reason being that both this.props.myObj and nextProps.myObj were referencing/pointing to the same object.
By implementing Immutable.js, the data will always be passed down as clones (instead of referencing the same object, they will actually be separate objects).
It re-enforces flux's uni-directional flow. You will never be able to (accidentally) modify the original data that is being passed into your component (whether as props or from a flux store).
This also enables you to use a PureRenderMixin – which automatically stops the render if the values of state/props have not changed. This could help with performance.
Finally I fixed it. The problem was changing the prop without deep cloning it.
my prop was an array of objects and i was changing it on parent with :
let newtodos = [...this.state.todos];
newtodos[0].text = 'changed text';
this.setState({ todos : newtodos });
So in componentDidUpdate i was getting the wrong result:
componentDidUpdate(prevProps){
//prevProps.todos is always equal to this.props.todos, since you changed the prop object directly.
if(JSON.stringify(prevProps.todos) !== JSON.stringify(this.props.todos){...}
}
So i fixed it with:
// let newtodos = [...this.state.todos]; // SHALLOW COPY - Wrong
let newtodos = JSON.parse(JSON.stringify(this.state.todos)); // Deep Clone - Right
newtodos[0].text = 'changed text';
this.setState({ todos : newtodos });
One alternative to implementing Immutable.js is to use object spread operator.
As an example,
suppose you have a variable that you want to modify then do something with
const obj1 = {a: 1, b: 2}
const obj2 = testObj
obj2.a: 3
// result: obj1 = obj2 = {a:3, b:2}
The original object will be modified. This is because the obj2 variable is essentially just a pointer to obj1 object.
Now suppose we use the spread operator instead
const obj1 = {a: 1, b: 2}
const obj2 = {...testObj}
obj2.a: 3
// result: obj1 = {a:1, b:2}, obj2 = {a:3, b:2}
The result of this will be that obj2 is properly updated but obj1 remains untouched. The spread operator effectively create a shallow clone of obj1. I have found the spread operator to be be particularly useful in modifying state in redux reducers. If you set variables equal to state directly and make modify them, it will cause the problem you were experiencing.
If you don't want to use spreading, you can do this instead. Just change your UpdateModel function to
UpdateModel(){
React.renderComponent(
<Grid boxes={ _.clone(_.pull(model, _.sample(model))) } />, document.body
);
}
Otherwise, the nextProp.boxes object with be the same object as the this.props.boxes object, so any alterations you made to it will be reflected in both.
Related
I'm using react native and my component is based on class base function. I'm facing difficulty in updating or adding object in an array..
My case :
I have an array:
this.state = {
dayDeatil:[]
}
now i want to add an obj in it but before that i want check if that object exist or not.
obj = { partition :1, day:"sunday","start_time","close_time",full_day:false}
in condition i will check partition and day if they both not match. then add an object if exist then update.
here is function in which i'm trying to do that thing.
setTimeFunc =(time)=>{
try{
console.log("time.stringify() ")
let obj = {
partition:this.state.activePartition,
day:this.state.selectedDay.name,
full_day:false,
start_time:this.state.key==="start_time"?time.toString():null
close_time:this.state.key==="close_time"?time.toString():null
}
let day = this.state.dayDetails.filter((item)=>item.day===obj.day&&item.partition===obj.partition)
if (day.length!==0) {
day[this.state.key]=time.toString()
this.setState({...this.state.dayDetail,day})
} else {
console.log("2")
this.setState({
dayDetails: [...this.state.dayDetails, obj]
})
}
this.setState({ ...this.state, clockVisiblity: false });
}
catch(e){
console.log("error -> ",e)
}
}
To check if the object exists or not, you can use Array.find() method, if it doesn't exists, the method will return undefined.
Now to update the state, the easier way would be to create a new array and set it as the new dayDetails state like this:
const { dayDetails } = this.state;
dayDetails.push(newData);
this.setState({ dayDetails: [...dayDetails] })
You should use the spread operator when setting the state because React uses shallow comparision when comparing states for a component update, so when you do [...dayDetails] you're creating a new reference for the array that will be on the state, and when React compares the oldArray === newArray, it will change the UI.
Also, after your else statement, you're updating the state with the state itself, it's good to remember that React state updates are asynchronous, so they won't be availabe right after the setState function call, this may be causing you bugs too.
Im using NGRX store on angular project.
This is the state type:
export interface TagsMap {
[key:number]: { // lets call this key - user id.
[key: number]: number // and this key - tag id.
}
}
So for example:
{5: {1: 1, 2: 2}, 6: {3: 3}}
User 5 has tags: 1,2, and user 6 has tag 3.
I have above 200k keys in this state, and would like to make the updates as efficient as possible.
The operation is to add a tag to all the users in the state.
I tried the best practice approach like so :
const addTagToAllUsers = (state, tagIdToAdd) => {
const userIds = Object.keys(state.userTags);
return userIds.reduce((acc, contactId, index) => {
return {
...acc,
[contactId]: {
...acc[contactId],
[tagIdToAdd]: tagIdToAdd
}
};
}, state.userTags);
};
But unfortunately this makes the browser crush when there are over 200k users and around 5 tags each.
I managed to make it work with this:
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let i = 0; i <= userIds.length - 1; i++) {
const currUserId = userIds[i];
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
And the components are updated from the store nicely without any bugs as far as I checked.
But as written here: Redux website mentions :
The key to updating nested data is that every level of nesting must be copied and updated appropriately
Therefore I wonder if my solution is bad.
Questions:
I Believe I'm still shallow coping all levels in state, am i wrong ?
Is it a bad solution? if so what bugs may it produce that I might be missing ?
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
What is the best efficient solution ?
Regarding question 3, here is an example of the selector code :
import {createFeatureSelector, createSelector, select} from '#ngrx/store';//ngrx version 10.0.0
//The reducer
const reducers = {
userTags: (state, action) => {
//the reducer function..
}
}
//then on app module:
StoreModule.forRoot(reducers)
//The selector :
const stateToUserTags = createSelector(
createFeatureSelector('userTags'),
(userTags) => {
//this will execute whenever userTags state is updated, as long as it passes the shallow check comparison.
//hence the question why is it required to return a new reference to every nested level object of the state...
return userTags;
}
)
//this.store is NGRX: Store<State>
const tags$: Observable<any> = this.store.pipe(select(stateToUser))
//then in component I use it something like this:
<tagsList tags=[tags$ | async]>
</tagsList>
Your solution is perfectly fine.
The rule of thumb is that you cannot mutate an object/array stored in state.
In Your example, the only thing that You are mutating is the stateUserTagsShallowCopy object (it is not stored inside state since it is a shallow copy of state.userTags).
Sidenote: It is better to use for of here since you don't need to access the index
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
If you decide to use immer this will look like this
import produce from "immer";
const addTagToAllUsers = (state, tagIdToAdd) => {
const updatedStateUserTags = produce(state.userTags, draft => {
for (let draftTags of Object.values(draft)) {
tags[tagIdToAdd] = tagIdToAdd
}
})
return updatedStateUserTags
});
(this comes usually with performance cost). With immer you can sacrifice performance to gain readability
ad 3.
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
Every store change selectors recompute to see if the dependent component should re-render.
imagine that instead of immutable update of the user tags we decided to mutate tags inside user (state.userTags is a new object reference but we mutate (reuse) old entries objects state.userTags[userId])
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId][tagIdToAdd] = tagIdToAdd;
}
return stateUserTagsShallowCopy;
};
In your case, you have a selector that takes out state.userTags.
It means that every time a state update happens nrgx will compare the previous result of the selector and the current one (prevUserTags === currUserTags by reference). In our case, we change state.userTags so the component that uses this selector will be refreshed with new userTags.
But imagine other selectors that instead of all userTags will select only one user tags. In our imaginary situation, we mutate directly userTags[someUserId] so the reference remains the same each time. The negative effect here is that subscribing component will be not refreshed (will not see an update after a tag is added).
I am struggling to get the real previous state of my inputs.
I think the real issue Which I have figured out while writing this is my use of const inputsCopy = [...inputs] always thinking that this creates a deep copy and i won't mutate the original array.
const [inputs, setInputs] = useState(store.devices)
store.devices looks like this
devices = [{
name: string,
network: string,
checked: boolean,
...etc
}]
I was trying to use a custom hook for getting the previous value after the inputs change.
I am trying to check if the checked value has switched from true/false so i can not run my autosave feature in a useEffect hook.
function usePrevious<T>(value: T): T | undefined {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
I have also tried another custom hook that works like useState but has a third return value for prev state. looked something like this.
const usePrevStateHook = (initial) => {
const [target, setTarget] = useState(initial)
const [prev, setPrev] = useState(initial)
const setPrevValue = (value) => {
if (target !== value){ // I converted them to JSON.stringify() for comparison
setPrev(target)
setTarget(value)
}
}
return [prev, target, setPrevValue]
}
These hooks show the correct prevState after I grab data from the api but any input changes set prev state to the same prop values.
I think my issue lies somewhere with mobx store.devices which i am setting the initial state to or I am having problems not copying/mutating the state somehow.
I have also tried checking what the prevState is in the setState
setInputs(prev => {
console.log(prev)
return inputsCopy
})
After Writing this out I think my issue could be when a value changes on an input and onChange goes to my handleInputChange function I create a copy of the state inputs like
const inputsCopy = [...inputs]
inputsCopy[i][prop] = value
setInputs(inputsCopy)
For some reason I think this creates a deep copy all the time.
I have had hella issues in the past doing this with redux and some other things thinking I am not mutating the original variable.
Cheers to all that reply!
EDIT: Clarification on why I am mutating (not what I intended)
I have a lot of inputs in multiple components for configuring a device settings. The problem is how I setup my onChange functions
<input type="text" value={input.propName} name="propName" onChange={(e) => onInputChange(e, index)} />
const onInputChange = (e, index) => {
const value = e.target.value;
const name = e.target.name;
const inputsCopy = [...inputs]; // problem starts here
inputsCopy[index][name] = value; // Mutated obj!?
setInputs(inputsCopy);
}
that is What I think the source of why my custom prevState hooks are not working. Because I am mutating it.
my AUTOSAVE feature that I want to have the DIFF for to compare prevState with current
const renderCount = useRef(0)
useEffect(() => {
renderCount.current += 1
if (renderCount.current > 1) {
let checked = false
// loop through prevState and currentState for checked value
// if prevState[i].checked !== currentState[i].checked checked = true
if (!checked) {
const autoSave = setTimeout(() => {
// SAVE INPUT DATA TO API
}, 3000)
return () => {
clearTimeout(autoSave)
}
}
}
}, [inputs])
Sorry I had to type this all out from memory. Not at the office.
If I understand your question, you are trying to update state from the previous state value and avoid mutations. const inputsCopy = [...inputs] is only a shallow copy of the array, so the elements still refer back to the previous array.
const inputsCopy = [...inputs] // <-- shallow copy
inputsCopy[i][prop] = value // <-- this is a mutation of the current state!!
setInputs(inputsCopy)
Use a functional state update to access the previous state, and ensure all state, and nested state, is shallow copied in order to avoid the mutations. Use Array.prototype.map to make a shallow copy of the inputs array, using the iterated index to match the specific element you want to update, and then also use the Spread Syntax to make a shallow copy of that element object, then overwrite the [prop] property value.
setInputs(inputs => inputs.map(
(el, index) => index === i
? {
...el,
[prop] = value,
}
: el
);
Though this is a Redux doc, the Immutable Update Patterns documentation is a fantastic explanation and example.
Excerpt:
Updating Nested Objects
The key to updating nested data is that every level of nesting must be
copied and updated appropriately. This is often a difficult concept
for those learning Redux, and there are some specific problems that
frequently occur when trying to update nested objects. These lead to
accidental direct mutation, and should be avoided.
a redux noob here.
In redux tutorial in this part particularly Why they didn't do something like that
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id === action.payload.todoId) {
todo.completed = !todo.completed
}
return todo
}
)
}
As far as I know map() doesn't have any side effects quoted from Mozilla docs
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
Why they did this extra step?
// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
Will it affect the functionality? or they just want to be consistent?
As far as I know map() doesn't have any side effects
It can have side-effects, that fact that the map method creates a new array doesn't mean it can't have any side effects. For example:
const arr = [{x: 1}, {x: 2}, {x: 3}];
const newA = arr.map(obj => {
obj.x += 1; // changes the objects in `arr`
return obj;
});
console.log(newA); // new array (objects are still references to original ones)
console.log(arr); // Also changed!
The map function gets passed a reference to each object, meaning that when you change obj, it also changes it within the original array. As a result, when you do:
todo.completed = !todo.completed
you're changing the todo objects in your array, which results in you changing your state directly.
Unlike the above, the presented code:
{
...todo,
completed: !todo.completed
}
does not change the todo object reference, but rather, it takes all the (enumerable own) keys from it and puts it in a new object, which also overwrites the completed key to hold the negated (ie: opposite) completed boolean value.
I've encountered a problem with the immutability of the state. Briefly, the Component's state has "persons" array of objects, each of them has a name property, which value I can update on onChange event.
Here is the updater
nameChangedHandler = (event, id) => {
const value = event.target.value;
const persons = this.state.persons.map((pers) => {
if (pers.id === id) {
pers.name = value;
}
return pers;
});
this.setState((prevState) => {
console.log(prevState);
return {
persons
}
});
}
This code works well for me and updates name property of a certain person, but I noticed that this.setState function mutates previous state.
I'm guessing that the reason may be in the map function. But how can it be? As far as I know, map takes the previos array and returns a new one. And this new array is later assigned to the persons property as a whole new array in setState function
.map creates a new array, but the object inside the array are still reference to the state's object.
One approach is to shallow copy the state's object to the new array:
const persons = this.state.persons.map( pers => {
// Do the shallow copy
if (pers.id === id) {
return {
...pers,
name: value
};
}
// Return the reference, because we don't do any mutations
return pers;
});
The above approach would work correctly, only if the Person objects are primitive values. Otherwise, you should do a deep copy.
Also you can check Immutable.js for safer dealing with immutable processing.