Reputation: 23
I'm new to React and fullstack dev in general so bear with me. I'm trying to concatenate data from a json-server (which I've received using Axios) to an array. I'm using React hooks to update state. For some reason, even when personObject shows up fine in the console (there are 4 elements each with a name and an ID), an empty array pops up when I console.log the persons array. Here's the useEffect code.
const [ persons, setPersons] = useState([])
useEffect(() => {
console.log('effect')
axios
.get('http://localhost:3001/persons')
.then(response => {
console.log('promise fulfilled')
const temp = response.data
temp.forEach(function(e) {
let personObject = {
content: e.name,
id: e.id,
}
console.log(personObject)
setPersons(persons.concat(personObject))
})
console.log(persons)
})
}, []) ```
Upvotes: 2
Views: 1360
Reputation: 2964
Updating state is asynchronous. For example:
const state = {
myValue: 1
}
const someFunctionThatUpdatesState = () => {
console.log("before", state.myValue)
setState({ myValue: 2})
console.log("after", state.myValue)
}
What do you think the console shows?
You might expect it to log before, 1
and after, 2
, but what actually gets logged is before, 1
, after, 1
.
When you set state, you are not actually setting state, you are scheduling a state update. You're basically telling React that state needs to be updated to the value you pass to setState
.
React doesn't actually update state until the component re-renders, so in your case, persons
will only have the new value once the function ends and the component updates.
Speaking of which, your function has a flaw:
setPersons(persons.concat(personObject))
Since persons
does not change until re-render, you are basically cancelling out your setState
call on every iteration. You're essentially doing this:
setPersons([].concat(personObject1))
setPersons([].concat(personObject2))
setPersons([].concat(personObject3))
setPersons([].concat(personObject4))
Your result will end up as an array with only the final value in it, all the rest are overwritten.
What you want is something like this:
.then(response => {
console.log('promise fulfilled')
// make sure data actually exists
if (response && response.data) {
// use prev, which is the up-to-date state value
setPersons(prev => [
// spread the current value of `prev`
...prev,
// map the data and spread the resulting array
...response.data.map(item => ({
id: item.id,
content: item.name,
})),
])
}
})
The changes I've made are:
data
is undefined
?map
instead of forEach
. What you're doing is mapping your data array to an array in state, so just use the map
function instead of forEach
.concat
with spread syntax ...
. It's more succinct and easier to read.setPersons
once, and pass a function to setPersons
.If your new state value depends on old state (like with persons
, you are appending the new data to the array of old data), you should pass a function to setState
. prev
is the most up-to-date state value, so you can depend on it. Doing something like setPersons(persons....)
(using persons
inside setPersons
is prone to errors.
Upvotes: 1
Reputation: 3598
It sounds like you're trying to fetch data, use that data to create a newPersons array, and add the newPersons to the end of the existing persons
array. See if this does the trick for you:
const [persons, setPersons] = useState([])
useEffect(() => {
console.log('effect');
axios
.get('http://localhost:3001/persons')
.then(response => {
console.log('promise fulfilled');
const newPersons = response.data.map(person => ({
content: person.name,
id: person.id
}));
setPersons(prev => [...prev, ...newPersons]);
});
}, []);
Upvotes: 1