const [obj, setObj] = useState{
id1: false,
id2: false,
...
})
const someFunc = async(id)=> {
setObj({...obj, [id]: true})
await longApiCall()
setObj({...obj, [id]: false})
}
This function gets called a couple times (or more), and this should set both to true then both to false, but they complete at close to the same time. Because setting state is asynchronous, each set state only flips one to false because the second one overwrites it back to true.
What's the best way to ensure the state object always gets the correct updates?
I know that I can use a ref or directly set the property on the obj without using the state setter, but I want to maintain the behavior that this state change causes a rerender.
// after calls are finished
obj = {
id1: true,
id2: false
}
//desired outcome
obj = {
id1: false,
id2: false
}
Use the function version of set state instead. React will pass you in the latest value of the state, and you can calculate the new state from that:
const someFunc = async(id)=> {
setObj(prev => {
return {...prev, [id]: true};
});
await longApiCall();
setObj(prev => {
return {...prev, [id]: false};
});
}
This is the cleanest solution I could come up with.
const objMimic = useRef({})
const someFunc = async(id)=> {
objMimic.current[id] = true
setObj({...obj, [id]: true})
await longApiCall()
objMimic.current[id] = false
setObj({...objMimic.current})
}
Related
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.
Goal: Update local form state using computed property names.
Problem: The computed property name does not overwrite/update my state. Instead it makes multiple copies
What it currently does: New objects get added to my array
const [channelOptions, setChannelOptions] = useState([
{ eml: false },
{ push: false },
{ inapp: false }
]);
Handler to update state when checkbox element is selected
const channelSelectionChangeHandler = (e) => {
setChannelOptions((prevState) => {
return [...prevState, { [e.target.value]: e.target.checked }];
});
};
Outcome - Multiple copies of inapp. The goal was to overwrite it
0:{eml: false}
1:{push: false}
2:{inapp: false}
3:{inapp: true}
4:{inapp: false}
5:{inapp: true}
What am I doing wrong? I think this is down to referencing.
Thanks!
If possible I will use an Object like { eml: false, push: false, inappropriate: false } instead of an array of objects.
Then I set new state with setChannelOptions(prev => ({...prev, [e.target.value]: e.target.checked }))
If you DO need to use array of objects, you can do it like this.
setChannelOptions( prevState => prevState.map( x => x[e.target.value] !== undefined ? { e.target.value: e.target.checked } : x )
});
What we do here is to .map through the array, check if the key exists (is defined). If the key exists, update with new value, else return the original object.
I am setting the state of a state variable in reactjs when a press a button. I call the following procedure (which is actually being called).
nextSuggestion() {
console.log(this.state.suggestions_index);
this.state.suggestions_index = this.state.suggestions_index + 1;
console.log(this.state.suggestions_index);
}
The above function is called and the page is not rerendered, even though I have the following componentDidMount():
async componentDidMount(){
const query = new URLSearchParams(this.props.location.search);
const token = query.get('token');
const url = "http://www.mocky.io/v2/" + token;
this.setState({url_prod: url});
const response = await fetch(url);
const data = await response.json();
this.setState({produto: data.suggestions[this.state.suggestions_index], loading_produto: false})
this.setState({alternativas: data.choices, loading_alternativas: false})
this.setState({atributos: data.suggestions[this.state.suggestions_index].product_data.attributes, loading_atributos: false})
}
The button is created through this code in render() function:
if(!this.state.loading_alternativas){
this.texts = lista_alternativas.map((text, key) => {
return <div id="center-button"><button type="button" className="btn btn-primary" key={text.id} onClick={this.nextSuggestion.bind(this)}>{text.text}</button></div>
});
}
the state:
state = {
produto: null,
alternativas: null,
atributos: [],
suggestions_index: 0,
loading_produto: true,
loading_alternativas: true,
loading_atributos: true,
showPopupDescProd: false,
showPopupAnswer: false,
url_prod: null,
};
What am I missing to make it work?
You're mutating state:
this.state.suggestions_index = this.state.suggestions_index + 1;
Which is a bad thing. More specifically, it's not expected by the ReactJS framework so it has no reason to re-render.
Don't set state values directly. Update the state with a new state object. For example:
this.setState((prevState) => ({
suggestions_index: prevState.suggestions_index + 1
}));
Calling setState() with notify the framework to replace the current state with the new one, triggering a re-render where applicable.
Edit: In response to Andy's comment (from which I learned something new, thanks Andy!), note also that setState() is asynchronous in nature. Since you're looking to log to the console after setting the state, you'd want to do that in a callback. For example:
const newState = {
...this.state
suggestions_index: this.state.suggestions_index + 1
};
this.setState(newState, () => console.log(this.state.suggestions_index));
(Though I suspect your console.log use is temporary for debugging only and will probably which removed, so you'd just omit the callback.)
I have this code in my constructor:
this.state = {
tests: [
{
question: "1",
answer: "2",
user: ""
},
{
question: "1",
answer: "2",
user: ""
},
],
};
I have edit function where I read event value in my input:
edit(id, event) {
this.state.tests[id].user = event.target.value;
this.setState({tests:this.state.tests});
}
But es give me this warning:
Do not mutate state directly. Use setState()
react/no-direct-mutation-state
What can i do in this case? Maybe somehow change the line with the assignment event.target.value into setState()?
You can use map() to create copy of tests
edit(id, event) {
const user = event.target.value;
const tests = this.state.tests.map((x,i) => i === id ? {...x, user} : x);
this.setState({tests});
}
One way I tend to go is to make a copy of the array first and then change an item in it, or change the array itself, and then set the state
var tests = this.state.tests.slice(0);
tests[id].user = event.target.value;
this.setState({tests:tests});
You may want to deep-clone the array in some cases, sometimes not.
You are correct, that the problem is with the line:
this.state.tests[id].user = event.target.value;
That's the point where you are mutating your state directly.
You have a few options.
You can "clone" the array first and then update it:
const newTests = [...this.state.tests];
newTests[id].user = event.target.value;
this.setState({tests: newTests});
You could also use immutability-helper:
const {value} = event.target;
this.setState(prevState => update(prevState, {[id]: {user: {$set: value}}}));
In this example, you need to extract value from your event, because it's not safe to access event values in asynchronous calls after an event has been handled.
edit(id, event) {
var newNote = {...this.state.tests[id]}
newNote.user = event.target.value
this.setState({ tests: [...this.state.tests, newNote]})
}
First of all, when you try to set a the new state using the data from previous state you have to use the updater as a function
https://reactjs.org/docs/react-component.html#setstate
const edit = (id, event) => {
this.setState((prevState) => {
const tests = [...prevState.tests];
tests[id] = {
...tests[id],
user: event.target.value
};
return {
...prevState,
tests
};
});
};
When state is not heavy, I use the following codes:
edit (id, event) {
const cp = JSON.parse(JSON.stringify(this.state.tests))
cp[id].user = event.target.value
this.setState({ tests: cp })
}
Update: I found Immer solves it perfectly.
import produce from 'immer'
edit (id, event) {
this.setState(
produce(draft => draft.tests[id].user = event.target.value)
)
}
state default values
state = {
moveType: {
value: 0,
open: false,
completed: false
}
};
// callback to update new state
let step = 'moveType';
let val = 3; // new value
let newObj = { ...this.state[step], value: val };
console.log(newObj);
this.setState({[step]: newObj }, function () {console.log(this.state);});
console.log(newObj) shows new values proper, but this.state still shows old values.. can you tell me what i'm doing wrong?
Setting state in react is pretty sensitive thing to do.
The best practices I've used to is always control object deep merge manually and use this.setState(state => { ... return new state; }) type of call, like in this example:
this.setState(state => ({
...state,
[step]: { ...(state[step] || {}), ...newObj },
}), () => console.log(this.state));
SNIPPET UPDATE start
[step]: { ...state[step], ...newObj }
Changed to:
[step]: { ...(state[step] || {}), ...newObj }
To deal correctly with cases, when state does not have this step key yet
SNIPPET UPDATE end
Thing is, that when you use this.state (in let newObj = { ...this.state[step]), it might have an outdated value, due to some pending (not merged yet) changes to the state, that you've called just couple of milliseconds ago.
Thus I recommend to use callback approach: this.setState(state => { ... use state and return new state;}) which guarantees that the state you use has latest value