Axios and looped promises - reactjs

I have problem with loop on axis GET request, and I can't understood why.
const [ state, setState ] = useState<any[]>([]);
ids.forEach((id) => {
getData(id)
.then((smth: Map<string, any>[]) => getNeededData(smth, id));
});
console.log(JSON.stringify(state));
and getData (getNeededData is only choose parameters):
export const getData= async (id: string) => {
const response = await Axios.get(`/rest/${id}`)
.then((res: { data: any; }) => res.data);
return response;
};
I should have 2 response (it's 2 id in variable "ids"), but I have first, second, first, second, first, and this in a loop.
Why it's been working like this?
What I can change for fix this?

By putting that forEach at the top level of your component function, you're running it every time the function is called by React to render its contents, which React does when state changes. The code you've shown doesn't set state, but I'm assuming your real code does.
To do it only when the component first mounts, wrap it in a useEffect callback with an empty dependency array:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
ids.forEach((id) => {
getData(id)
.then(/*...*/);
});
}, []);
If all of the results are going in the state array, you probably want to use map and Promise.all to gether them all up and do a single state change with them, for instance:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
Promise.all(
ids.map((id) => {
return getData(id).then(/*...*/);
})
)
.then(allResults => {
// Use `allResults` to set state; it will be an array in the same order
// that the `id` array was in
})
.catch(error => {
// handle/report error
});
}, []);

Related

useState not updating an array correctly

I'm using React with Firebase to fetch data from firestore and display it to the user. Within a useEffect hook, I fetch the data and add it to an array of json objects. However, when adding a new element to the array, the previous one gets deleted. Below will be relevant portions of code.
const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(true);
const { currentUser } = useAuth();
function addItemToList(i) {
const updatedList = [
...items,
{
data: i.data(),
ref: i.ref
}
]
return updatedList;
}
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
itemSnapshot.forEach((doc) => {
setItems(addItemToList(doc));
})
})
setLoading(false);
}, []);
I've also tried updating the items list within the useEffect hook, but it produces the same bug.
Let me take a moment to explain what is happening here.
Your have to think in your react component as a "Snapshot", where a snapshot is the result of a render. Each snapshot points to a state created by useState and only to that one. What does this mean?
The first time your component renders (first snapshot) the items variable returned by useState is pointing to an empty array and as long as that snapshot is alive it would be pointing to that array.
So, lets take a look to the function called by useEffect
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
itemSnapshot.forEach((doc) => {
setItems(addItemToList(doc));
})
})
setLoading(false);
you are calling the set function for items once per elemtent in firebase query result and each call is seting a value that is the result of calling addItemToList
function addItemToList(i) {
const updatedList = [
...items,
{
data: i.data(),
ref: i.ref
}
]
return updatedList;
}
the point here is that you are always destructuring ...items which is always the items variable that the snapshot of the component is pointing to and it would not be updated until the component re rederds.
So, basically you all aways doing updatedList = [ ...[], { ... } ] becauses items has to wait to take update his value.
#Viet already responded with something good, which I think it's the best option
mapping the result of the query to an array and updating the array with the result of the map a parameter.
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
const result = [];
itemSnapshot.forEach((doc) => {
result.push({
data: i.data(),
ref: i.ref
});
})
setItems(result);
})
setLoading(false);
}, []);
You need to add "addItemToList" to the dependencies of the useEffect hook.
Because you haven't done this, the effect references an old version of that function which references an old version of the items array.
You just use itemSnapshot.map like this:
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
setItems(
itemSnapshot.map((doc) => ({
data: i.data(),
ref: i.ref,
})),
);
})
Instead of running it for each value maybe you should try to map it as a list and update the state once
function addItemsToList(newItems) {
const updatedList = [
...items,
...newItems
]
return updatedList;
}
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
const docs = itemSnapshot.map((doc) => ({
data: doc.data(),
ref: doc.ref
});
setItems(addItemsToList(docs));
})
setLoading(false);
}, []);
You are using spread operator in a wrong way! This way will not update the array.
You can do that by using es6 spread to concat multiple arrays something like below:
const updatedList = [
...items,
...[{ data: i.data(), ref: i.ref }]
]
To know more: Using es6 spread to concat multiple arrays

Why do I need to put the function inside a setState method for it to work?

When a socket emits an event from the server side the App reloads for some reason and posts is emptied out. But when I define the function inside the setPosts it works perfectly. Why is this?
const App = () => {
let [user, setUser] = useState(null)
let [posts, setPosts] = useState({})
console.log('app')
useEffect(() => {
console.log('use effect')
socket.on('post', (post) => {
// THIS DOES NOT WORK:
// let newPosts = { ...posts }
// newPosts[post._id] = post
// setPosts(newPosts)
//THIS WORKS
setPosts((posts) => {
let newPosts = { ...posts }
newPosts[post._id] = post
return newPosts
})
})
async function getUser() {
let user = await actions.getUser()
if (user) {
setUser(user?.data)
}
}
getUser()
actions
.getAllPosts()
.then((res) => {
console.log('WE GOT ALL POSTSTFOM API', res.data)
const postsById = {}
for (let post of res.data) {
postsById[post._id] = post
}
console.log('wired')
setPosts(postsById)
//filterPosts(res.data)
})
.catch((err) => console.error(err))
return () => {
socket.off('post')
}
}, [])
This is how enclosures work in javascript. When you use a non-functional state update you are referencing the posts state value ({}) from the render cycle the callback was instantiated in, i.e. the initial render when the effect callback ran when mounted (note the empty dependency array). It's a stale enclosure of the posts state value.
When using a functional state update you are accessing and updating from the previous state, not the state from the previous render cycle (or enclosure).

UseEffect infinite loop with array of dependencies

I'm having an issue with useEffect and useState. I'm trying to fill a state with data from an api, but it results in an infinite loop, even if I use an array with dependencies. It works when I try to get a name. The problem occurs when I try to get an array or an object.
Here the code:
const id = props.match.params.id;
const [pokemon, setPokemon] = useState({});
useEffect(() => {
let cancelRequest;
axios
.get(`https://pokeapi.co/api/v2/pokemon/${id}`, {
cancelToken: new axios.CancelToken(
(cancel) => (cancelRequest = cancel)
),
})
.then((res) => {
setPokemon(res.data);
console.log(pokemon);
})
.catch((err) => {
console.log(`ERROR:: ${err.message}`);
});
return () => {
cancelRequest();
};
}, [id, pokemon]);
Here a sample of data from the console:
{abilities: Array(2), base_experience: 64, forms: Array(1), game_indices: Array(20), height: 7, …}
Thank you.
Do not use the axios request inside the useEffect.
Create another function for this and use useCallback. For example:
const fetchPokemon = useCallback(() => {
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`)
.then((res) => {
setPokemon(res.data);
})
.catch(() => {}
}, [id])
useEffect(() => {
fetchPokemon()
}, [fetchPokemon])
If you pass in pokemon into the dependency array, it will update every single time you call setPokemon since the pokemon update aka, you have an infinte loop.

value of state is always default. React js 16.12.0

I have two useEffect-s. One is used to fetch data from api and save it in the state and second is called only once and it starts listening to websocket event.
In the websocket event handler I log the fetched data but it always has the default value.
Even though fetching data completes successfully and the list is drawn on UI, the value of list is always empty - [].
const [list, setList] = useState([]);
useEffect(() => {
axios.get("https://sample.api.com/get/list")
.then(res => {
setList(res.data);
});
}, [window.location.pathname.split('/')[2]]);
useEffect(() => {
webSocket.on('messageRecieved', (message) => {
console.log(list);
});
}, []);
Your second effect is referencing the initial list value (an empty array) due to closure. This is why useEffect should reference all of its dependencies in its second argument.
But in this case, where you don't want to subscribe to the webSocket event each time the list is updated, you could use React's refs on the list.
const listValue = useRef([]);
const [list, setList] = useState(listValue.current);
When setting the value:
res => {
listValue.current = res.data
setList(listValue.current);
}
And when retrieving the list in a one time fired useEffect:
useEffect(() => {
webSocket.on('messageRecieved', (message) => {
console.log(listValue.current);
});
}, []);
try changing
.then(res => {
to
.then((res) => {
Would clarify if you added console logs to each hook or said if the values are preset in them:
useEffect(() => {
axios.get("https://sample.api.com/get/list")
.then((res) => {
console.log(res.data)
setList(res.data);
});
}, [window.location.pathname.split('/')[2]]);
useEffect(() => {
webSocket.on('messageRecieved', (message) => {
console.log(list);
console.log(message);
});
}, []);
You could also add error catch, just in case:
.catch((error) => {
console.log(error.response)
})

React state hook doesn't properly handle async data

I'm trying to set a component's state through an effect hook that handles the backend API. Since this is just a mock, I'd like to use the vanilla react methods and not something like redux-saga.
The problem is that while the fetching part works, the useState hook doesn't update the state.
const [odds, setOdds] = useState({})
useEffect(() => {
(async () => {
fetchMock.once('odds', mocks.odds)
let data = await fetch('odds').then(response => response.json())
setOdds(data)
console.log(odds, data) // {}, {...actual data}
})()
}, [])
I've tried to pipe the whole process on top of the fetch like
fetch('odds')
.then(res => res.json())
.then(data => setOdds(data))
.then(() => console.log(odds)) // is still {}
But it doesn't make a single difference.
What am I doing wrong?
Basically if you call setOdds, the value of odds does not change immediately. It is still the last reference available at decleration of the hook.
If you want to access the new value of odds after updating it, you would have to either use the source of the updated value (data) if you want to access the value in the same useEffect hook or create another useEffect hook that triggers only when odds has changed:
useEffect(() => {
console.log(odds);
// Do much more
}, [odds]) // <- Tells the hook to run when the variable `odds` has changed.
If you want to see that state has changed in here, you can use
const [odds, setOdds] = useState({})
useEffect(() => {
(async () => {
fetchMock.once('odds', mocks.odds)
let data = await fetch('odds').then(response => response.json())
setOdds(prevData => {
console.log(prevData, data) // {}, {...actual data}
return data
})
})()
}, [])

Resources