useState value is undefined inside async function - javascript

I have a custom hook that saves/loads from cacheStorage.
const useCache = (storageKey, dir) => {
const [cache, setCache] = useState()
useEffect(() => {
const openCache = async () => {
const c = await caches.open(storageKey)
setCache(c)
}
openCache()
},[])
const save = async (res) => {
console.log(cache)
//prints undefined
cache.put(dir, new Response(JSON.stringify(res)))
const load = async () => {
console.log('load', cache)
//prints undefined
const res = await cache.match(dir)
return await res.json()
}
return { save, load }
}
and this useCache hook is used inside of another custom hook useConfigs
const useConfigs = (key, defaultValue = false) => {
const { save, load } = useCache('configs', '/configs')
(...)
const getConfigs = async () => {
const res = await fetchFromNetwork()
save(res)
}
const getLocalConfigs = async () => {
const res = await load()
(...)
return res
}
The issue is that useState variable cache returned null when both save() and load() are called inside getConfigs() and getLocalConfigs(). Seems like the value is undefined because of closure. If that is the reason, what would be the solution to update the cache variable by the time save() and load() are called?

Related

React js setstate not working in nested axios post

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned.
I just need to assign the value from 'res.data.id' to 'metricId' so that I can use it somewhere else in another function like save2() function.
Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
This part of code run sync
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
but there you run async
axios.get(api1, getDefaultHeaders())
.then(() => {
You can refactor your code to async/await like this:
const save1 = async () => {
const response = await axios.get(api1, getDefaultHeaders());
const response2 = await axios.post(api2, { item1: response.data.item1 }, getDefaultHeaders());
return response2.data.id;
};
const save2 = (activeId) => {
console.log(activeId); // result: null
};
const handleSaveAll = async () => {
const activeId = await save1();
save2(activeId);
setActiveId(activeId);
console.log(activeId); // result: again its still null
};
or to chain of promises, like this:
const save2 = (activeId) => {
console.log(activeId); // result: null
};
const save1 = () => {
return axios.get(api1, getDefaultHeaders())
.then(({ data }) => {
const data = {item1: item1,};
return axios.post(api2, {item1: data.item1}, getDefaultHeaders())
})
.then((res) => res.data.id);
};
const handleSaveAll = () => {
save1()
.then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
return res.data.id;
})
.then(save2);
};

Call async method and use its response inside useRef in React

I am trying to call Async function and use its response inside of useRef in react like the below:
const myComponent = () => {
const getTitle = async () => {
const res = await fetch('title');
return res;
}
const myref = useRef(
Employee().retire({
id: 1,
title: await getTitle(),
}),
);
}
However, I cannot use await inside of useRef. What other way to accomplish this?
Set the ref
const myRef = useRef(null);
And then use useEffect to update it once when the component renders:
// `getTitle` doesn't need to be async because
// you're apparently already returning a promise
function getTitle() {
return fetch('title');
}
useEffect(() => {
// Call an async function in your `useEffect`
// to wait for the promise to resolve and use the data
// to update the ref
async function getData() {
const title = await getTitle();
myRef.current = Employee().retire({ id: 1, title });
}
getData();
}, []);
The only way I know is set myref after async function is solved. Something like:
const myComponent = () => {
const getTitle = async () => {
const res = await fetch('title');
return res;
}
const myref = useRef(null);
useEffect(() => {
(async () => {
let title = await getTitle();
myref.current = Employee().retire({ id: 1, title });
console.log(myref.current);
})();
}, []);
}
Here a working example.
await keyword using only inside of async function

React customHook using useEffect with async

I have a customHook with using useEffect and I would like it to return a result once useEffect is done, however, it always return before my async method is done...
// customHook
const useLoadData = (startLoading, userId, hasError) => {
const [loadDone, setLoadDone] = useState(false);
const loadWebsite = async(userId) => {
await apiService.call(...);
console.log('service call is completed');
dispatch(someAction);
}
useEffect(() => {
// define async function inside useEffect
const loadData = async () => {
if (!hasError) {
await loadWebsite();
}
}
// call the above function based on flag
if (startLoading) {
await loadData();
setLoadDone(true);
} else {
setLoadDone(false);
}
}, [startLoading]);
return loadDone;
}
// main component
const mainComp = () => {
const [startLoad, setStartLoad] = useState(true);
const loadDone = useLoadData(startLoad, 1, false);
useEffect(() => {
console.log('in useEffect loadDone is: ', loadDone);
if (loadDone) {
// do something
setStartLoad(false); //avoid load twice
} else {
// do something
}
}, [startLoad, loadDone]);
useAnotherHook(loadDone); // this hook will use the result of my `useLoadData` hook as an execution flag and do something else, however, the `loadDone` always false as returning from my `useLoadData` hook
}
It seems in my useDataLoad hook, it does not wait until my async function loadData to be finished but return loadDone as false always, even that I have put await keyword to my loadData function, and setLoadDone(true) after that, it still returns false always, what would be wrong with my implementation here and how could I return the value correct through async method inside customHook?
Well...it seems to be working after I put the setLoadDone(true); inside my async method, not inside useEffect, although I am not sure why...
updated code:
// customHook
const useLoadData = (startLoading, userId, hasError) => {
const [loadDone, setLoadDone] = useState(false);
const loadWebsite = async(userId) => {
await apiService.call(...);
console.log('service call is completed');
dispatch(someAction);
setLoadDone(true);
}
useEffect(() => {
// define async function inside useEffect
const loadData = async () => {
if (!hasError) {
await loadWebsite();
}
}
// call the above function based on flag
if (startLoading) {
await loadData();
// setLoadDone(true); doesn't work here
}
}, [startLoading]);
return loadDone;
}

How to fetch data from multiple urls at once?

I have a function that fetches from a url in React
const DataContextProvider = (props) => {
const [isLoading, setLoading] = useState(false);
const [cocktails, setCocktails] = useState([]);
useEffect(() => {
const fetchCocktailList = async () => {
const baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1/';
setLoading(true);
try {
const res = await fetch(`${baseUrl}search.php?s=margarita`);
const data = await res.json();
console.log(data);
setCocktails(data.drinks);
setLoading(false);
} catch (err) {
console.log('Error fetching data');
setLoading(false);
}
};
fetchCocktailList();
}, []);
How I'm mapping data so far.
const DrinkList = () => {
const { cocktails } = useContext(DataContext);
return (
<div className='drink-list-wrapper'>
{cocktails.length > 0 &&
cocktails.map((drink) => {
return <DrinkItem drink={drink} key={drink.idDrink} />;
})}
</div>
);
};
However I want to fetch from this url also ${baseUrl}search.php?s=martini
I would like a good clean way to do this and set my state to both of the returned data.
First base the data fetch function on a parameter:
const fetchCocktail = async (name) => {
const baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1/';
try {
const res = await fetch(`${baseUrl}search.php?s=` + name);
const data = await res.json();
return data.drinks;
} catch (err) {
console.log('Error fetching data');
}
}
Then use Promise.all to await all results:
setLoading(true);
var promises = [
fetchCocktail(`margarita`),
fetchCocktail(`martini`)
];
var results = await Promise.all(promises);
setLoading(false);
DrinkList(results);
Where results will be an array with the responses that you can use on the DrinkList function.
Here's a method which will let you specify the cocktail names as dependencies to the useEffect so you can store them in your state and fetch new drink lists if you want new recipes. If not, it'll just be a static state variable.
I've also added another state variable errorMessage which you use to pass an error message in the case of failure.
Also, you should include the appropriate dependencies in your useEffect hook. The setState functions returned by calls to useState are stable and won't trigger a re-run of the effect, and the cocktailNames variable won't trigger a re-run unless you update it with new things to fetch.
const DataContextProvider = (props) => {
const [isLoading, setLoading] = useState(false);
const [cocktails, setCocktails] = useState([]);
const [errorMessage, setErrorMessage] = useState(''); // holds an error message in case the network request dosn't succeed
const [cocktailNames, setCocktailNames] = useState(['margarita', 'martini']); // the search queries for the `s` parameter at your API endpoint
useEffect(() => {
const fetchCocktailLists = async (...cocktailNames) => {
const fetchCocktailList = async (cocktailName) => {
const baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1/search.php';
const url = new URL(baseUrl);
const params = new URLSearchParams({s: cocktailName});
url.search = params.toString(); // -> '?s=cocktailName'
const res = await fetch(url.href); // -> 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s=cocktailName'
const data = await res.json();
const {drinks: drinkList} = data; // destructured form of: const drinkList = data.drinks;
return drinkList;
};
setLoading(true);
try {
const promises = [];
for (const cocktailName of cocktailNames) {
promises.push(fetchCocktailList(cocktailName));
}
const drinkLists = await Promise.all(promises); // -> [[drink1, drink2], [drink3, drink4]]
const allDrinks = drinkLists.flat(1); // -> [drink1, drink2, drink3, drink4]
setCocktails(allDrinks);
}
catch (err) {
setErrorMessage(err.message /* or whatever custom message you want */);
}
setLoading(false);
};
fetchCocktailList(...cocktailNames);
}, [cocktailNames, setCocktails, setErrorMessage, setLoading]);
};
var promises = [
fetchCocktail(api1),
fetchCocktail(api2)
];
var results = await Promise.allSettled(promises);

Async/await in componentDidMount to load in correct order

I am having some troubles getting several functions loading in the correct order. From my code below, the first and second functions are to get the companyID companyReference and are not reliant on one and another.
The third function requires the state set by the first and second functions in order to perform the objective of getting the companyName.
async componentDidMount() {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
a;
b;
c;
}
componentWillUnmount() {
this.isCancelled = true;
}
companyIdParams = () => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
});
};
getCompanyReference = () => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
});
});
};
getCompanyName = () => {
const { firebase } = this.props;
const { companyID, companyReference } = this.state;
const cid = companyID;
if (companyReference.includes(cid)) {
const getCompany = firebase.company(cid);
getCompany.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyName: doc.data().companyName,
loading: false
});
});
} else if (cid !== null && !companyReference.includes(cid)) {
navigate(ROUTES.INDEX);
}
};
How can I achieve this inside componentDidMount?
setState is asynchronous, so you can't determinate when the state is updated in a sync way.
1)
I recommend you don't use componentDidMount with async, because this method belongs to react lifecycle.
Instead you could do:
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
}
2)
The companyIdParams method doesn't have a return, so you are waiting for nothing.
If you need to wait I would return a promise when setState is finished;
companyIdParams = () => {
return new Promise(resolve => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
}, () => { resolve() });
});
};
The same for getCompanyReference:
getCompanyReference = () => {
return new Promise(resolve => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
}, () => { resolve() });
});
});
};
3)
If you want to parallelize the promises, you could change the previous code to this:
const [a, b] = await Promise.all([
await this.companyIdParams(),
await this.getCompanyReference()
]);
4)
According to your code, the third promise is not a promise, so you could update (again ;) the above code:
const [a, b] = .....
const c = this.getCompanyName()
EDIT: the bullet points aren't steps to follow
As the last api call is dependent on the response from the first 2 api calls, use a combination of Promise.all which when resolved will have the data to make the last dependent call
async componentDidMount() {
let [a, c] = await Promise.all([
this.companyIdParams(),
this.getCompanyReference()
]);
const c = await this.getCompanyName();
}

Categories