I'm developing a react-native/nodeJS project and I'm experiencing issues with the Axios API call to my backend using async/await functions.
Here's the code:
const TimeTable = () => {
const [attendedCourses, setAttendedCourses] = useState([]);
const [courseSchedules, setCourseSchedules] = useState([]);
useEffect(() => {
getUserCourses();
getCourseSchedule();
console.log(courseSchedules);
}, []);
const getCourseSchedule = async () => {
for (const item of attendedCourses) {
try {
const res = await axios.get(`/api/lesson/findById/${item.courseId}`);
setCourseSchedules((prev) => [
...prev,
{
id: res.data._id,
name: res.data.name,
schedule: [...res.data.schedule],
},
]);
} catch (err) {
const error = err.response.data.msg;
console.log(error);
}
}
};
const getUserCourses = async () => {
const userId = "12345678"; //hardcoded for testing purpose
try {
const res = await axios.get(`/api/users/lessons/${userId}`);
setAttendedCourses(res.data);
} catch (err) {
const error = err.response.data.msg;
console.log(error);
}
};
return (...); //not important
};
export default TimeTable;
The method getUserCourses() behave correctly and returns always an array of objects which is saved in the attendedCourses state. The second method getCourseSchedule() doesn't work correctly. The console.log() in the useEffect() prints most of the time an empty array.
Can someone please explain to me why? Thank you!
While the method is async, the actual useEffect is not dealing it in asynchronous manner and won't await till you reach the console.log in the useEffect. If you put the console.log inside the getCourseSchedule method and log the result after the await, it'll show you correct result every time.
You are confusing the async nature of each method. Your code does not await in the useEffect, it awaits in the actual method while the rest of the useEffect keeps executing.
If you really want to see the result in useEffect, try doing:
useEffect(() => {
const apiCalls = async () => {
await getUserCourses();
await getCourseSchedule();
console.log(courseSchedules);
}
apiCalls()
})
Your useEffect has an empty array as dependencies that means it is run only onetime in before initial render when the courseSchedules has initial value (empty array)
To see the courseSchedules when it change you should add another useEffect like this:
useEffect(() => {
console.log(courseSchedules);
}, [courseSchedules]);
Related
I am doing an API call which is returning IDs and based on number of ids I am doing another call and trying to combine the responses but I am stuck with async issues.
const SearchUser = async () => {
try {
const response = await getSearchUsers();
const ids = response.data?.map((user) => user.userId);
await ids.forEach(async (id) => {
const result = await getUserInfo(id);
setRNOUsers(...result);
// combine result in one state
});
} catch (error) {
setSearching(false);
}
};
useEffect(() => {
SearchUser();
console.log('RNOUsers', RNOUsers); // this is empty and runs even before callng api
}, []);
How can handle this?
You can use Promise.all to wait for all responses, and then set them together with setRNOUsers
const SearchUser = async () => {
try {
const response = await getSearchUsers();
const ids = response.data?.map((user) => user.userId);
const responses = await Promise.all(ids.map(id => getUserInfo(id)))
setRNOUsers(...responses.flatMap(x => x));
} catch (error) {
setSearching(false);
}
};
useEffect(() => {
SearchUser();
console.log('RNOUsers', RNOUsers);
}, []);
Side note, the problem with console.log('RNOUsers', RNOUsers) is setRNOUsers (initialized by useState) is asynchronous. Besides that, your API calls are also asynchronous, so you cannot get values from RNOUsers immediately in useEffect. If you want to see data in that log, you should wait until the state is updated and your component gets re-rendered with your latest data.
I have a React app built with the Minimal template and I'm trying to follow along with one of their tutorials, in order to create a Redux slice that feeds some data to a custom component. The data itself is collected from Firebase. Below is my code:
firebase.js - helper
export function getDocuments(col) {
const colRef = collection(db, col);
const q = query(colRef, where('uid', '==', auth.currentUser.uid));
getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
// return [1,2,3]
}
product.js - Redux slice
export function getProducts() {
return async (dispatch) => {
dispatch(slice.actions.startLoading());
try {
const products = await getDocuments('products');
dispatch(slice.actions.getProductsSuccess(products));
} catch (error) {
dispatch(slice.actions.hasError(error));
}
};
}
ProductList.js - component
const dispatch = useDispatch();
const { products } = useSelector((state) => state.client);
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
useEffect(() => {
if (products.length) {
// setTableData(products);
}
}, [products]);
If I console log data in the helper function (firebase.js), I get the values I expect, once the promise is resolved/fulfilled. However, if I console.log clients in the product.js slice or later in the component, I get undefined.
I assume my problem is not being able to understand how async + await + useEffect work together in order to fix this. My assumption is that I am trying to access the value before the promise is resolved and therefore before the helper function returns it. I confirmed that by returning a simple array [1, 2, 3] in my helper function as a test.
I think I am missing something fundamental here (I am not very experienced with React and JS in general and still learning things on the go). Can someone help me understand what am I doing wrong?
Thank you!
With await you can await the fulfillment or rejection of a promise, but your getDocuments Function does not return a promise. Change the last line of the function to the following:
return getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
Async and Await are no different in React than in plain JavaScript:
When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method
useEffect():
By using this Hook, you tell React that your component needs to do something after rendering. This function will run every time the component is re-rendered.
I am getting data in an asynchronous way and consequently showing it inside the render() function using {data}.
Now my question is, how do I let the render() function wait until the variable is defined? Currently either the placeholder variable doesn't change or it never arrives.
This is my code,
let [data] = useState();
let storeData = async (value) => {
try {
await AsyncStorage.setItem('locatie', value)
} catch (e) {
console.log(e)
}
}
let getInfo = async () => {
data = await AsyncStorage.getItem('locatie');
return data;
}
useEffect(() => {
getInfo().then(r => console.log('getInfo called, data var = ' + data))
})
console.log('return() initiate, data var = ' + data)
And showing it here:
<TextInput value={data !== undefined ? data : 'placeholder'} onChangeText={(value) => storeData(value)}/>
Evidently, since it's asynchronous, render() happens before the function.
Any help would be appreciated. Please no flame I use React Native first time :)
Also, please note that it's inside a function, I am not using classes and will not use them.
You should learn more about using react hooks. Anyway, you can refer to below for your issue.
const [data, setData] = useState('');
const storeData = async (value) => {
try {
setData(value);
await AsyncStorage.setItem('locatie', value);
} catch (e) {
console.log(e);
}
};
const getInfo = async () => {
const res = await AsyncStorage.getItem('locatie');
setData(res);
};
useEffect(() => {
getInfo();
}, []);
Instead of let [data] = useState();
write const [data, setData] = useState();
and update the state inside the useEffect hook inside a then()
in your case, after the promise returns some data.
Below is the code to fetch some data
useEffect(async (id) => {
// const data = await redditFetch(id);
async function redditFetch(id){
try {
const {data:{data}} = await axios.get(`https://www.reddit.com/search.json?q=${id}&limit=50`);
const {children} = data;
return children.map(data=> data.data);
} catch (error) {
console.log(error);
}
}
const data = redditFetch(id);
setData(data)
},[setData])
console.log(Data);
And this is the promise returned:
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Array(50)
What I want is the Array inside the PromiseResult.
I've also tried another away using async await like this which returns a proper array.
const PostPage = () => {
const {id} = useParams();
const [Data, setData] = useState([])
useEffect(async (id) => {
// const data = await redditFetch(id);
async function redditFetch(id){
try {
const {data:{data}} = await axios.get(`https://www.reddit.com/search.json?q=${id}&limit=50`);
const {children} = data;
return children.map(data=> data.data);
} catch (error) {
console.log(error);
}
}
const data = await redditFetch(id);
setData(data)
},[setData])
console.log(Data);
But with this comes a bloody warning that I can't get rid of:
index.js:1 Warning: An effect function must not return anything
besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a
Promise. Instead, write the async function inside your effect and call
it immediately:
useEffect(() => { async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ... } fetchData(); }, [someId]); // Or [] if effect doesn't need props or state
So, my dear coders, tell me what to do???
I did a sample demo with the post code:
CODESANDBOX: https://codesandbox.io/s/flamboyant-nobel-p87mt9?file=/src/App.js:257-265
In your code const data = redditFetch(id); the async functions will return a Promise. You have to use await, but React will warn you to not transform the useEffect callback in async function
You can remove the API function of your useEffect.
Don't put the setDate in your useEffect dependency array. In this case, you can leave this empty to run just one time. Also, don't put the date because will re-render in an infinite loop. If you need some conditional just remove the dependency array and put some conditional to run setDate.
I had to mock the id in the query parameter to be possible to work with API
CODE:
import axios from "axios";
import { useEffect, useState } from "react";
import "./styles.css";
async function redditFetch() {
try {
const {
data: { data }
} = await axios.get(`https://www.reddit.com/search.json?q=01&limit=50`);
const { children } = data;
return children.map((data) => data.data);
} catch (error) {
console.log(error);
}
}
export default function App() {
const [data, setData] = useState([]);
const Data = data.map((redditContent) => {
return (
<div key={redditContent.name}>
<h2>{redditContent.title}</h2>
<hr />
</div>
);
});
useEffect(() => {
(async function () {
const redditResponse = await redditFetch();
console.log(redditResponse);
setData(redditResponse);
})();
}, []);
return <div className="App">{Data}</div>;
}
ALTERNATIVE: CLEANER APPROACH (No anonymous function)
If you don't like of anonymous function approach, you can use this one:
async function updateRedditPosts() {
const redditResponse = await redditFetch();
console.log(redditResponse);
setData(redditResponse);
}
useEffect(() => {
updateRedditPosts();
}, []);
Anything return from useEffect supposed to be clean up while component unmount. Better you may invoke setData function there instead of return.
On the other hand, you may use immediate invoke function (IIF) then you do not need to resolve the promise that return from redditFetch function.
After an input change in my input element, I run an empty string check(if (debouncedSearchInput === "")) to determine whether I fetch one api or the other.
The main problem is the correct promise returned faster than the other one, resulting incorrect data on render.
//In my react useEffect hook
useEffect(() => {
//when input empty case
if (debouncedSearchInput === "") autoFetch();
//search
else searchvalueFetch();
}, [debouncedSearchInput]);
searchvalueFetch() returned slower than autoFetch() when I emptied the input. I get the delayed searchvalueFetch() data instead of the correct autoFetch() data.
What are the ways to tackle this? How do I queue returns from a promises?
I read Reactjs and redux - How to prevent excessive api calls from a live-search component? but
1) The promise parts are confusing for me
2) I think I don't have to use a class
3) I would like to learn more async/await
Edit: added searchvalueFetch, autoFetch, fetcharticles code
const autoFetch = () => {
const url = A_URL
fetchArticles(url);
};
const searchNYT = () => {
const url = A_DIFFERENT_URL_ACCORDING_TO_INPUT
fetchArticles(url);
};
const fetchArticles = async url => {
try{
const response = await fetch(url);
const data = await response.json();
//set my state
}catch(e){...}
}
This is an idea how it could looks like. You can use promises to reach this. First autoFetch will be called and then searchvalueFetch:
useEffect(() => {
const fetchData = async () => {
await autoFetch();
await searchvalueFetch();
};
fetchData();
}, []);
You can also use a function in any lifecycle depends on your project.
lifecycle(){
const fetchData = async () => {
try{
await autoFetch();
await searchvalueFetch();
} catch(e){
console.log(e)
}
};
fetchData();
}
}