I'm trying to update the state of an array from out of a forEach loop without loosing the previous state. I`m trying to archive something like the following:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
let currentState = request;
request[idx] = Object.assign(...request[idx], {answer: response.answer});
setRequests(currentState);
})
})
}
But in such a case only one response will be rendered. Any idea how to archive something like this?
CodePudding user response:
The Problems
There are a couple of issues there:
- You're breaking one of the fundamental rules of React state: Do not modify state directly. Doing
const currentState = request;
doesn't copy the object, it just makes two variables point to the same object. - You're using the version of state setter where you just pass in the update, which completely overwrites any previous outstanding state updates.
Solutions
There are (at least) two approaches here:
Do piecemeal updates as you are in that code, where earlier updates are stored in state (causing a re-render) while later updates are still in progress
or
Get all the updated information, and do a single state update (and render)
Both are valid depending on your use case.
Piecemeal Updates
For the piecemeal updates, since you're doing a bunch of state updates (which may not occur in order, depending on the vagaries of the timing of the fetch
replies). Since you're updating state based on previous state (the other entries in the array), you need to use the callback version of the state setter. Then, you need to create a new array each time, and and new object within the array for the object at index idx
, like this:
// *** Note I've renamed `request` to `requests` -- it's an array, it should
// use the plural, and you use `request` for an individual one later.
// I've also reversed `response` and `data`, since they were backward in the
// original. I've also added (minimal) handling of errors.
const [requests, setRequests] = useState(initialState);
const run = () => {
requests.forEach((request, idx) => {
// (It seems odd that nothing from `request` is used in the `fetch`)
fetch("/ask")
.then((response) => response.json())
.then((data) => {
setRequests((prevRequests) =>
prevRequests.map((request, index) => {
return idx === index ? { ...request, answer: data.answer } : request;
})
);
})
.catch((error) => {
// ...handle/report error...
});
});
};
map
creates the new array, returning a new object for the one at index idx
, or the previous unchanged ones for the others.
All At Once
The other approach is to do all the fetch
calls, wait for them all to complete, and then do a single state update, like this:
// (Same naming and error-handling updates)
const run = () => {
Promise.all(requests.map((request) =>
fetch("/ask")
.then((response) => response.json())
.then((data) => {
return {...request, answer: data.answer};
})
))
.then(setRequests)
.catch((error) => {
// ...handle/report error...
});
};
CodePudding user response:
You can build the new array at once and update it, something like this
const run = () => {
Promise.all(request.map((r) => fetch("/ask").then(data => data.json())))
.then((responses) => {
const newRequest = responses.map(res => ({answer: res.answer}))
setRequests(newRequest);
})
}
Using Promise.all allows you to retrieve all the json response at once, so you can build the new state in one shot.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
note: code not tested.
CodePudding user response:
You problem stems from the fact that let currentState = request
will keep a reference to the request
at the time of making the closure
. This means that it won't get updated by future calls.
You need to use the functional
version of setState
. Something like this:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
setRequests(currentState => {
currentState[request[idx]] = {answer: response.answer};
return {...currentState};
});
})
})
}