I have the following function to set state:
const [study, setStudy] = useState(defaultState)
const setValue = (section, key, value) => {
setStudy({...study, [section]: {...study[section], [key]: value}})
}
But for some reason it keeps overriding the existing state, what am I doing wrong?
I call the function like this:
setValue('company', 'name', 'test')
setValue('property', 'state', 'test2')
setValue('property', 'address', 'test3')
Structure of data:
const defaultState: StudyData = {
client: {},
company: {},
property: {},
study: {}
}
I think the problem is that the study value you destruct is still an old version (a memoized one). It is the same between all setValue calls and thus your first two setValue calls do essentially nothing. If this is what's happening to you, the best way to fix it would be to give a callback function to setStudy. Any function given to a setter will get the absolute latest version of the state as parameter. Try it like this:
setStudy((latestStudy) => {
return {
...latestStudy,
[section]: {...latestStudy[section], [key]: value}
}
});
When dealing with multiple state-updating events, the standard behavior for React is to batch them. The state-updating method does not immediately update the state of the component, React just puts the update in queue to be processed later when event handling is complete. Changes are all flushed together at the end of the event and you don't see the intermediate state.
With batching, when the final set-state event executes, it has no recollection that the prevState has been updated (asynchrounous), which is why the final state appears to only reflect changes made by the last event.
Using a call-back function as the first argument for your setStudy method will help you return the intermediate states and avoid the issue of batching. The updater function will loop through and shallowly merge all updated states with the previous component state. This ensures that we receive all state-updates and use all intermediate states before ultimately arriving at the final update.
const setValue = (section, key, value) => {
setStudy(study => {
return {
...study,
[section]: { ...study[section], [key]: value }
};
});
};
Also see working sandbox: https://codesandbox.io/s/delicate-tree-vqrz7
Related
So i have a const with an array of objects, named "data", a state that receive that data so I can update the screen when I change it, and I have a function that filter that data.
Every time that the user change the state of an HTML select, I trigger a function named handleChange that calls the filter function, so the user user can see the filtered content.
The problem is that I want to reset the state that receives the data, with the original data before filtering it, so if the user change one of the selects, it filter based on the original data, not the previous changed one, but when I try to update the state with the const data value, it doesn't work.
Here is my code
const [list, setList] = useState<IData[]>([...data]);
const [filter, setFilter] = useState<IFilter>({
name: "",
color: "",
date: "",
});
function handleChange(
key: keyof IData,
event: React.ChangeEvent<{ value: string }>
): void {
const newFilter = { ...filter };
newFilter[key as keyof IData] = event.target.value;
setList([...data]); // reset the data
setFilter({ ...newFilter }); // set the filter value
filterList();
}
function filterList(): void {
const keys = Object.keys(filter) as Array<keyof IData>;
keys.forEach((key: keyof IData) => {
if (filter[key]) {
const newList = list.filter((item: IData) => item[key] === filter[key]);
setList([...newList]);
}
});
}
the problem is here
setList([...data]); // reset the data
setFilter({ ...newFilter }); // set the filter value
filterList();
apparently, when the filterList happens, the list state is not yet restarted with the data value, since if I console log it inside filterList(), it return me only the list that was previous filtered. Is there a way for me to make sure that the setList happens before filtering, so I'm sure that the list that is filtered is always based on the initial value?
You can access the updated values inside useEffect (more about useEffect), so instead of calling filterList() directly inside the handler you move it inside the hook:
React.useEffect(()=>{filterList();},[list,filter])
Alternatively, you can pass the values as parameters to the function, after you updated the state with them
filterList(newList, newFilter)
UPDATE
As you update list inside filterList you'll need some sort of a flag to indicate if you need to filter of not (I'd use useRef). Note that it would be preferable to pass parameters into filterList instead, because this way there will be one less rendering cycle. Here is a simplified example of how both can work, let me know if they make sense : https://jsfiddle.net/6xoeqkr3/
I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}
I have created a <Form> Component that makes all the form-fields you give to it controlled by injecting value and onChange props to the form-fields by iterating through them. This has been working perfectly well for me for most of the forms I have created.
When I needed to have the functionality of the form-field values controlling some aspect of the parent state, I added a onFormValueChange prop to the Form that would get called whenever a field value gets updated. Using this I can track a subset of the changes to the Form's state.
However, now my problem is this...how do I override the value of a form-field, conditional on some event that occurs in the parent. I have not been able to figure out how to override the Form's state just once. If I give it a prop that sets an override value like {name: string, value: any}, then on every update this override value will override the form-field's value which is not good.
These are the solutions I thought of but they seem extremely hacky and I was hoping someone in the SO community can help.
Set an override prop on the Form Component which times out after around 100ms and hope that the user doesn't try to modify the form in that tiny duration. But I dislike using setTimeout for hacks like these.
Pass a disableOverride function along with the overrideValue prop. Then in my Form's shouldComponentUpdate I can just call disableOverride() in the callback of the setState I will use to override the value. Something like:
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.override) {
const { name, value } = nextProps.override;
const newState = Object.assign({}, this.state, { [name]: value });
this.setState(newState, () => {
nextProps.disableOverride();
});
return false;
}
return true;
}
But this also feels unnecessarily complicated, and possibly quite vulnerable to crashing unexpectedly.
EDIT Some further clarification: The point of this would be so that for example if I have 'country' and 'city' fields then if 'country' is cleared, then the 'city' should get cleared too. I can track the state of 'country' with onFormValueChange but don't have an API to modify the Form state in order to clear the 'city' field.
I came up with a solution. It was absurdly simple, I dont know why it took me so long.
componentDidUpdate(prevProps, prevState) {
if (!isEqual(prevProps.override, this.props.override)) {
const { name, value } = nextProps.override;
this.setState({
[name]: value
});
}
}
isEqual is an object comparison function taken from lodash
So, I have a checkbox having onchange method but in the method, I am not able to change the value.
Also, the value received is the same as key in the state variable.
Following the code:
constructor(props){
super(props)
this.state={pins:[],filter:{Irrigation:true,Patvan:true,Drinking:true,Minigrid:true,Rooftop:true}}
this.handleFilterChange=this.handleFilterChange.bind(this)
}
handleFilterChange(filtervalue){
console.log(filtervalue) //lets assume value to be Drinking
console.log(this.state.filter[filtervalue]) // it says true
this.setState({filter[filtervalue]:!this.state.filter[filtervalue]}) //here the error occurs
}
I know i am doing some syntax mistake. Help in pointing it out. Thanks
You can't update the state using filter[filtervalue] as property key, you should do something like this:
this.setState(
prevState => ({
...prevState,
filter: {
...prevState.filter,
[filtervalue]: !prevState.filter[filtervalue]
}
})
)
First, if you need to access the previous state during an update of the state, you should use the method tecnique, not the object one.
Then, updating an inner property of the state can be a bit cumbersome as you see, my suggestion is to use something like immer.
First of all when your state changes depend on the current state use the updater method of setState. After that since you are updating a nested object you need to recreate the nesting up to the property you need to update.
In your case the object passed to setState should be
{
filter: {
Irrigation: true,
Patvan: true,
Drinking: false, // this is what changed
Minigrid: true,
Rooftop: true
}
}
So your actual setState call should be
this.setState(({
filter // destructure the original state and extract the filter property
}) => ({
filter: {
...filter, // spread the existing filter properties
[filtervalue]: !filter[filtervalue] // overwrite the property that changed
}
}))
can anyone lemme know the working of each line as i dont have any idea
can we have let in set sate? please explain these 2 lines
const userDetails = previousState.userDetails
return { userDetails: {...userDetails, [key]: value} }
the return in set state i have not understand
this.setState((previousState) => {
const userDetails = previousState.userDetails
return { userDetails: {...userDetails, [key]: value} }
})
setState can accept a function from the previous state to a new state:
this.setState(previousState => newState);
previousState => newState is an arrow function, that will accept the previous state (which will be provided by setState when it is invoked), and has to provide the new state.
In the code you posted, the new state is calculated by taking an object from the previous state called userDetails, and then appending a new key-value pair to it.
This is done using two mechanisms. The first, is the spread operator: ...userDetails which adds to the new userDetails value all of the key-value pairs that were in the original object. Next, the new value is added:
[key]: value
This means that a new pair is to be created, where the key is the value of the key variable (I suspect this is available somewhere in the function before the setState call), and the value is equal to a variable named value, which again is previously available.
This is the notation to use when you want to use a dynamic value as a key in a map.
Edit - Answering question from comment
Any function body that is more than one line needs to be put in a block { ... }. When you want to return a value from that function, a return keyword is used.
The return you see in your setState is the return from the function that is passed to setState.