Getting previous State of useState([{}]) (array of objects) - javascript

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.

Related

How to update or add an array in react.js?

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.

Redux how to update nested state object immutably without useless shallow copies?

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

ReactJs a state is triggeing in a loop when I use a selector normal function

Main problem: I tried to use a function inside the selector to reestructur the data and join another variable, in this case my group and put together with their children as items, the problem is that the function is called every time on an infinite loop despite the state is not being altered.
I have this selector:
const groups = useSelector(state => selectProductGroups(state));
And the function is this one:
const groups = state.PlatformsReducer.groups;
const items = state.PlatformsReducer.items;
return groups.reduce((ac, g) => {
g.items = items.filter(i => i.groupId == g.productNumber);
if (ac[g.platformId]) {
ac[g.platformId].push(g);
} else {
ac[g.platformId] = [g];
}
return ac;
}, {});
};
So when I use a useEffect to detect if the groups variable has changed the useEffect is triggered in a loop despite the variable groups still empty.
Do you know why? or How to prevent this.
I now the problem is the function in the selector, but I don't know how to prevent this case.
This has to do with what the useSelector hook does internally.
useSelector runs your selector and checks if the result is the same as the previously received result (reference comparison). If the results differ then the new result is stored and a rerender is forced. If the results are the same then the old result is not replaced and no rerender is triggered.
What this does mean is that every time the store updates, even if it is an unrelated part of the state, your complex function will be run to determine whether the result has changed. In your case it is always a new reference and therefore always a change.
I think the best way to handle this is to keep your selectors as simple as possible, or use some form of more complex memoization like provided by reselect.
Below is an example of how you might be able to keep your selectors simple but still achieve an easy way to reuse your product group selection using a custom hook.
const useProductGroups = () => {
// Get groups from the store.
// As the selector does not create a new object it should only
// trigger a rerender when groups changes in the store.
const groups = useSelector(state => state.PlatformsReducer.groups);
// Get items from the store,
// As the selector does not create a new object it should only
// trigger a rerender when items changes in the store.
const items = useSelector(state => state.PlatformsReducer.items);
// Reduce the group collection as desired inside of a useMemo
// so that the reduction only occurs when either items or groups
// changes.
const productGroups = useMemo(() => {
return groups.reduce((ac, g) => {
g.items = items.filter(i => i.groupId == g.productNumber);
if (ac[g.platformId]) {
ac[g.platformId].push(g);
} else {
ac[g.platformId] = [g];
}
return ac;
}, {});
}, [groups, items] /* dependency array on items / groups */);
// return the calculated product groups
return productGroups;
}
You can then use the custom hook in your function components:
const groups = useProductGroups();

React setstate not merging the old state into the new state

according to many examples, this should work:
const [_timeseries, $timeseries] = useState({hi:'lol'})
useEffect(() => {
socket.on('plot', e => {
let keyname = Object.keys(e)[0]
$timeseries({..._timeseries, [keyname] : value)})
}
}, [])
console.log(_timeseries) // here results in the initial state, not the set state
The first time it merges, it works.
But once a new event with another keyname enters, it replaces the whole thing again.
Instead of adding a new key with [keyname], the old [keyname] is being replaced.
The problem here is closures.
The callback assigned to the useEffect closes the initial value of _timeseries in it's the lexical scope and it never updated.
To fix it, you need to use the functional useState which uses the most updated state within its callback:
const [_timeseries, $timeseries] = useState({hi:'lol'})
useEffect(() => {
socket.on('plot', e => {
let keyname = Object.keys(e)[0]
$timeseries(timeSeries => {...timeseries, [keyname] : value)})
}
}, [])
The useState hook gives you a function which replaces the state entirely with a new value (doesn't merge it): https://reactjs.org/docs/hooks-state.html
However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
You can use setState with a function and merge it yourself:
$timeseries((old) => ({...old, [keyname] : value)}))
If you use it without a function it might have the old values (because you don't specify it as a dependency of useEffect)

How to modify a 'value' prop in VueJS before `$emit('input')` finishes updating it

I have a question about creating VueJS components that are usable with v-model which utilise underlying value prop and $emit('input', newVal).
props: {
value: Array
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.value
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.value
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.$emit('input', newArray)
}
}
In the above code sample, if I do two modifications in quick succession, they will both be executed onto the "old array" (the non-modified value prop).
moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
In other words, I need to wait for the value to be updated via the $emit('input') in order for the second call to moveIdToIndex to use that already modified array.
Bad solution 1
One workaround is changing updateArray to:
updateArray (newArray) {
return new Promise((resolve, reject) => {
this.$emit('input', newArray)
this.$nextTick(resolve)
})
}
and execute like so:
await moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
But I do not want to do this, because I need to execute this action on an array of Ids and move them all to different locations at the same time. And awaiting would greatly reduce performance.
Bad solution 2
A much better solution I found is to just do this:
updateArray (newArray) {
this.value = newArray
this.$emit('input', newArray)
}
Then I don't need to wait for the $emit to complete at all.
However, in this case, VueJS gives a console error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
Does anyone have any better solution?
OK. These are your options as far as I understand your use case and application.
First of all, don't mutate the props directly save the props internally and then modify that value.
props: {
value: Array
},
data() {
return {
val: this.value
}
}
If the next modification to the array is dependent on the previous modification to the array you can't perform them simultaneously. But you need it to happen fairly quickly ( i will assume that you want the user to feel that it's happening quickly ). What you can do is perform the modification on the val inside the component and not make it dependent on the prop. The val variable is only initialized when the component is mounted. This way you can modify the data instantly in the UI and let the database update in the background.
In other words, your complete solution would look like this:
props: {
value: Array
},
data () {
return {val: this.value}
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.val
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.val
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.val = newArray
this.$emit('input', newArray)
}
}
This solution fixes your problem and allows you to execute moveIdToIndex in quick succession without having to await anything.
Now if the array is used in many places in the application next best thing would be to move it to a store and use it as a single point of truth and update that and use that to update your component. Your state will update quickly not simultaneously and then defer the update to the database for a suitable time.
Emit a message to the parent to change the prop.
Put a watcher on the prop (in the child) and put your code to use the new value there.
This keeps the child from mutating the data it does not own, and allows it to avoid using nextTick. Now your code is asynchronous and reactive, without relying on non-deterministic delays.
How about making copy of the value ?
moveIdToIndex (id, newIndex) {
const valueCopy = [...this.value]
const newArrayHead = this.valueCopy
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.valueCopy
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)

Categories

Resources