While the functional setState() is recommended when the current state is being used, the function still cannot be async. How could we develop a function that uses the state, calls a REST API and changes the state right after?
This is the original function, that disregards the async behavior of setState.
onSortColumnChanged = (sortColumn) => async (event) => {
const prev = this.state;
const searchParams = { ...prev.params, sortColumn: sortColumn };
const result = await this.callSearch(searchParams);
this.setState({ params: searchParams, result: result });
}
If we change the above function to use functional setState, VSCode complains that await can only be used in async functions.
onSortColumnChanged = (sortColumn) => (event) => {
this.setState((prev) => {
const searchParams = { ...prev.params, sortColumn: sortColumn };
const result = await this.callSearch(searchParams);
return { params: searchParams, result: result };
});
}
I think I'm missing something fundamental here.
Short answer
onSortColumnChanged = (sortColumn) => async (event) => {
const searchParams = { ...this.state.params, sortColumn: sortColumn };
const result = await this.callSearch(searchParams);
this.setState(prev => {
prev.params.sortColumn = sortColumn
prev.result = result
return prev
})
}
Long answer
Why use setState callbacks
The React docs state that multiple calls to setState may be batched together.
Let's pretend you have a chat app. You might have a function that looks like this:
async incoming_message(message){
this.setState({messages: this.state.messages.concat(message)})
}
So if 2 messages come in a few milliseconds apart, React may batch these two calls to setState()
this.setState({messages: [].concat(message_1)}) // 1
this.setState({messages: [].concat(message_2)}) // 2
and when 2 runs, we will lose message_1.
Providing setState with a transform function is the solution
async incoming_message(message){
this.setState(prev => {
prev.messages.push(message)
return prev
})
}
In the callback-based setState, the 1st message will not be lost, since instead of specifying an exact state to write, we specify a way to transform the old state into the new state.
Important notes
The object version of setState() does a shallow merge of the new state and the previous state. This means that calling setState() with an object will only update the keys that you pass in.
If you choose to use the callback version of setState() instead, you are expected to return the entire new state. So when you have
return { params: searchParams, result: result };
in your setState() callback, you will lose all state value except params and result.
Your code
the functional setState() is recommended when the current state is being used
It is important to understand why the functional setState() is recommended. If your code will not break when multiple setState() calls are batched together, it is OK to pass an object instead of a function.
If you still want to use the callback pattern, you don't need to do all the work in your setState() callback, you only need to specify how to transform the old state into the new one.
onSortColumnChanged = (sortColumn) => async (event) => {
const {params} = this.state;
const searchParams = { ...params, sortColumn: sortColumn };
const result = await this.callSearch(searchParams);
this.setState(prev => {
// prev is the old state
// we changed sortColumn param
prev.params.sortColumn = sortColumn
// and we changed the result
prev.result = result
// and we return the new, valid state
return prev
// or if you prefer an immutable approach
return Object.assign({}, prev, {
params: {...prev.params, sortColumn: sortColumn},
result: result,
})
})
}
Think of 'await' as a callback function of promise. So you don't have promise here and you are calling callback function. Hence, VSCode ask you to add the promise which in this case it is 'async' function.
Related
i am using usestate for transfer data. but ufotunately it not quite work.
here is my code:
const [totCons, settotCons] = useState(null)
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch('https://piscons2.vercel.app/ConsPiscTotCons');
// convert the data to json
const json = await data.json();
// set state with the result
settotCons(json);
console.log(json)
console.log(totCons)
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [])
as you can see on image the json return data but the totCons return null.
i did set it settotCons(json)
Updated state will not be available to the state value immedieately.
The react setState is asynchronous, but thats not the only reason for this behaviour. The reason is a closure scope around an immutable const value.
Both props and state are assumed to be unchanging during 1 render.
Treat this.state as if it were immutable.
You can use useEffect to create the sideeffects for totCons
useEffect(() => {
// action on update of totCons
}, [totCons]);
try doing console.log(totCons) outside useEffect.
you will not get the updated value in next line.
you will get the updated value in next render
I got a problem with a component. I make a n number calls to an external API and then get back values inside an object. I want to add each of this object return in the state. However, each object is duplicate in my state. I try to put conditions but nothing work. I just started with React and Node so I'm sorry if the solution is obvious...
//Id for the API calls
const Ref = ['268', '294']
const nbreLiens = Ref.length
//For each Id we make an API call, get the response, and set the state without erasing the previous value
Ref.map(lien=>{
const Tok = localStorage.getItem('token');
Axios.get("http://localhost:3001/liens/sup/"+Tok+"/"+lien).then((Response) => {
//We shouldn't get more entries that the number of Id's
if (test.length<nbreLiens){
setTest(test=>[...test,Response.data])
}
})
})
}, [])
Try resolving all the API call Promises first, then add the resposes to the state.
(Im not handling errors)
//Id for the API calls
const Ref = ['268', '294']
useEffect(() => {
const getElements = async (idsToGet) => {
const Tok = localStorage.getItem('token');
const allPromises = idsToGet.map(id => Axios.get("http://localhost:3001/liens/sup/" + Tok + "/" + id))
const dataElements = await Promise.all(allPromises)
const responseDataElements = dataElements.map(response => response.data)
setTest(() => responseDataElements)
}
getElements(Ref);
}, [])
Seems to me that your statement if (test.length<nbreLiens){ will not work as you are expecting.
In resume: test.length will always be the same even after calling setTest inside this function.
From react docs:
In React, both this.props and this.state represent the rendered
values, i.e. what’s currently on the screen.
Calls to setState are asynchronous - don’t rely on this.state to
reflect the new value immediately after calling setState. Pass an
updater function instead of an object if you need to compute values
based on the current state (see below for details).
You can read more about this at: https://reactjs.org/docs/faq-state.html
I couldn't find a similar question here, so here it goes:
I created a custom hook useBudget to fetch some data.
const initalState = {
budget_amount: 0,
};
const useBudget = (resource: string, type: string) => {
const [budgetInfo, setBudget] = useState(initalState);
useEffect(
() => {
(async (resource, type) => {
const response = await fetchBudgetInfo(resource, type);
setBudget(response);
})(resource, type);
}, []);
return [budgetInfo];
};
And on the component that uses that hook, I have something like this:
const [budgetInfo] = useBudget(resource, type);
const [budgetForm, setBudgetForm] = useState({ warningMsg: null, errorMsg: null, budget: budgetInfo.budget_amount });
The problem is: The initial state of this component does not update after the fetching. budget renders with 0 initially and keeps that way. If console.log(budgetInfo) right afterwards, the budget is there updated, but the state is not.
I believe that this is happening due to the asynchronicity right? But how to fix this?
Thanks!
I could get to a fix, however, I am not 100% that this is the best/correct approach. As far as I could get it, due to the asynchronicity, I am still reading the old state value, and a way to fix this would be to set the state inside useEffect. I would have:
const [budgetInfo] = useBudget(resource, type);
const [appState, setAppState] = useState({ budget: budgetInfo.budget_amount });
useEffect(
() => {
setAppState({ budget: budgetInfo.budget_amount });
}, [budgetInfo]);
But it's working now!
Working example: https://stackblitz.com/edit/react-wiewan?file=index.js
Effects scheduled with useEffect don’t block the browser from updating the screen - that's why 0 (initialState) is displayed on the screen. After the value is fetched, the component stays the same as there is no change in its own state (budgetForm).
Your solution updates component's state once budgetInfo is fetched hence triggering a re-render, which works but seems to be rather a workaround.
useBudget could be used on its own:
const useBudget = (resource, type) => {
const [budgetInfo, setBudget] = useState(initalState);
const fetchBudgetInfo = async () => {
const response = await (new Promise((resolve) => resolve({ budget_amount: 333 })))
setBudget(response)
}
useEffect(() => {
fetchBudgetInfo(resource, type)
}, [])
return budgetInfo
};
const App = props => {
const { budget_amount } = useBudget('ok', 'ok')
return (
<h1>{budget_amount}</h1>
);
}
fetchBudgetInfo is split out since if we make effect function async (useEffect(async () => { ... })), it will be implicitly returning a promise and this goes against its design - the only return value must be a function which is gonna be used for cleaning up. Docs ref
Alternatively, consider retrieving data without a custom hook:
const fetchBudgetInfo = async (resource, type) => {
const response = await fetch(resource, type)
return response
}
useEffect(() => {
const response = fetchBudgetInfo(resource, type)
setAppState((prevState) => { ...prevState, budget: response.budget_amount });
}, []);
Notably, we are manually merging old state with the new value, which is necessary if appState contains several values - that's because useState doesn't shallowly merge objects (as this.setState would) but instead completely replaces state variable. Doc ref
On a somewhat related note, there is nothing wrong with using object to hold state, however using multiple state variables offers few advantages - more precise naming and the ability to do individual updates.
I have the following function:
fetchData = () => {
fetch('/api/customerNodes' + this.state.number)
.then(response => response.text())
.then(message => {
this.setState({responseText: message});
});
};
If I point my browser to localhost:3000/1234 and I want to get the pathname of 1234, I do something like this. const num = window.location.pathname;
The problem I face is im not able to set that value to state.
componentDidMount() {
const num = window.location.pathname;
this.setState({ number: num });
console.log(this.state.number);
//setInterval(this.fetchData, 250);
}
The console.log is empty.
What am I doing wrong?
Use the callback function in this.setState
const num = window.location.pathname;
this.setState({ number: num }, () => {
console.log(this.state.number, 'number');
});
setState() is usually asynchronous, which means that at the time you console.log the state, it's not updated yet.By putting the log in the callback of the setState() method it is executed after the state change is complete.
setState is asynchronous and you can't expect it to execute before the next line of code.
From the docs:
React may batch multiple setState() calls into a single update for performance.
setState is async method. If you log state in render method, you can get the number.
As we've known, setState is async. I've read few questions about setState, on how to use the value right after setState, but those aren't what I need right now.
I'm trying to set value for array List, and then use that List to do a function to get the value for Result. If setState isn't async, then it would be like this
`
handleChange(e) {
const resultList = this.state.list.slice();
resultList[e.target.id] = e.target.value;
this.setState({
list: resultList,
result: this.doSomething(resultList) // this.doSomething(this.state.list)
});
}
`
Is there anyway to achieve this? A documentation or keyword to research would be awesome.
Many thanks
There is a callback parameter to setState which is called after the state has been updated
this.setState({
list: resultList,
result: this.doSomething(resultList)
}, () => {
//do something with the updated this.state
});
You can use async await like
async handleChange(e) {
const resultList = this.state.list.slice();
resultList[e.target.id] = e.target.value;
this.setState({
list: resultList,
result: await this.doSomething(resultList) // this.doSomething(this.state.list)
});
}
The use of this.state together with this.setState is discouraged, exactly because state updates are asynchronous and may result in race conditions.
In case updated state derives from previous state, state updater function should be used. Because it's asynchronous, event object should be treated beforehand, due to how synthetic events work in React:
const { id, value } = e.target;
this.setState(state => {
const list = [...state.list];
list[id] = value;
return {
list,
result: this.doSomething(list)
};
});