I have created a custom hook to fetch data. In that hook I have an async function looking something like this:
const useData = () => {
const [enabled, setEnabled] = useState(true);
const [data, setData] = useState(false);
const fetchData = async () => {
const response = await fetch();
const data = await response.json();
while (data.continue && enabled) {
response = await fetch(data.requestId);
data = await response.json();
}
};
useEffect(() => {
console.log(enabled);
}, [enabled]);
return { data, setEnabled };
};
When I call setEnabled(false) from a component it's set to false when the useEffect logs it but it continues to be true in the fetchData function and it never cancels the fetch which I was expecting.
This is because the fetchData callback function "captures" the state of the data variable when it is first called, and it doesn't know about the new value of data when you change it (this is known in React as stale closures). To fix this, you have to use the useRef hook instead:
const useData = () => {
const [enabled, setEnabled] = useState(true);
const [data, setData] = useState(false);
const isCancelled = useRef(false);
const fetchData = async () => {
const response = await fetch();
const data = await response.json();
while (data.continue && !isCancelled.current) {
response = await fetch(data.requestId);
data = await response.json();
}
};
useEffect(() => {
isCancelled.current = !enabled;
}, [enabled]);
return { data, setEnabled };
};
Related
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?
I have problem with async function. I need track.user in another function but my func getTracks() async. I don't have clue how can i get this.
const Player = ({trackUrl, index, cover, id}) => {
const [track, setTrack] = useState({})
const [user, setUser] = useState({})
useEffect(() => {
const getTracks = async () => {
await httpClient.get(`/track/${id}`)
.then((response) => {
setTrack(response.data);
})
}
getTracks();
getUser() // track.user undefined
}, [])
const getUser = async() => {
await httpClient.get(`/profile/${track.user}/`)
.then((response) => {
setUser(response.data);
})
}
}
I would declare both functions at the beginning of the component (you can later optimise them with useCallback but it's not that important in this phase).
const getTracks = async () => {
await httpClient.get(`/track/${id}`)
.then((response) => {
setTrack(response.data);
})
}
const getUser = async() => {
await httpClient.get(`/profile/${track.user}/`)
.then((response) => {
setUser(response.data);
})
}
I would then call an async function inside the useEffect hook. There are a couple of ways of doing it: you can either declare an async function in the useEffect hook and call it immediately, or you can call an anonymous async function. I prefer the latter for brevity, so here it is:
useEffect(() => {
(async () => {
await getTracks();
getUser();
})();
}, []);
Now when you call getUser you should be sure that getTracks has already set the track variable.
Here is the complete component:
const Player = ({trackUrl, index, cover, id}) => {
const [track, setTrack] = useState({})
const [user, setUser] = useState({})
const getTracks = async () => {
await httpClient.get(`/track/${id}`)
.then((response) => {
setTrack(response.data);
})
}
const getUser = async() => {
await httpClient.get(`/profile/${track.user}/`)
.then((response) => {
setUser(response.data);
})
}
useEffect(() => {
(async () => {
await getTracks();
getUser();
})();
}, []);
}
EDIT 07/18/22
Following Noel's comments and linked sandbox, I figured out that my answer wasn't working. The reason why it wasn't working is that the track variable was't available right after the getTrack() hook execution: it would have been available on the subsequent render.
My solution is to add a second useEffect hook that's executed every time the track variable changes. I have created two solutions with jsonplaceholder endpoints, one (see here) which preserves the most of the original solution but adds complexity, and another one (here) which simplifies a lot the code by decoupling the two methods from the setTrack and setUser hooks.
I'll paste here the simpler one, adapted to the OP requests.
export default function Player({ trackUrl, index, cover, id }) {
const [track, setTrack] = useState({});
const [user, setUser] = useState({});
const getTracks = async () => {
// only return the value of the call
return await httpClient.get(`/track/${id}`);
};
const getUser = async (track) => {
// take track as a parameter and call the endpoint
console.log(track, track.id, 'test');
return await httpClient.get(`profile/${track.user}`);
};
useEffect(() => {
(async () => {
const trackResult = await getTracks();
// we call setTrack outside of `getTracks`
setTrack(trackResult);
})();
}, []);
useEffect(() => {
(async () => {
if (track && Object.entries(track).length > 0) {
// we only call `getUser` if we are sure that track has at least one entry
const userResult = await getUser(track);
console.log(userResult);
setUser(userResult);
}
})();
}, [track]);
return (
<div className="App">{user && user.id ? user.id : "Not computed"}</div>
);
}
You can move the second request to the then block of the dependent first request,i.e., getTracks.
Also, you shouldn't mix then and await.
useEffect(() => {
const getTracks = () => {
httpClient.get(`/track/${id}`)
.then((response) => {
setTrack(response.data);
httpClient.get(`/profile/${response.data.user}/`)
.then((response) => {
setUser(response.data);
})
})
}
getTracks();
}, [])
You shouldn't be mixing thens with async/await. You should be using another useEffect that watches out for changes in the track state and then calls getUser with that new data.
function Player(props) {
const { trackUrl, index, cover, id } = props;
const [ track, setTrack ] = useState({});
const [ user, setUser ] = useState({});
async function getTracks(endpoint) {
const response = await httpClient.get(endpoint);
const data = await response.json();
setTrack(data);
}
async function getUser(endpoint) {
const response = await httpClient.get(endpoint);
const data = await response.json();
setUser(data);
}
useEffect(() => {
if (id) getTracks(`/track/${id}`);
}, []);
useEffect(() => {
if (track.user) getUser(`/profile/${track.user}`);
}, [track]);
}
I have two API's
First API returns list of items which I am iterating to get each item's detailed data.
Here's the code
const [loader, setLoader] = useState(false);
React.useEffect(() => {
const fetchUsers = async() => {
setLoader(true);
const users = await getUsers();
const promises = users.map(async (user) => {
let userData = await getUsersDetailedData(user.userId);
return userData
});
let finalUsers = await Promise.all(promises);
setLoader(false);
}
fetchUsers();
}, [])
I am updating loader state before the api call and after call but it is not working.
Loader state is updating these many times and loader is not displaying
logs
Try it in this way,
React.useEffect(() => {
const fetchUsers = async() => {
const users = await getUsers();
const promises = users.map(async (user) => {
let userData = await getUsersDetailedData(user.userId);
return userData
});
let finalUsers = Promise.all(promises);
return finalUsers;
}
setLoader(true);
fetchUsers().then(res=>{
setLoader(false);
});
}, [])
I have a component which displays products for a category. CategoryId is taken from subscribe method which is formed by pubsub pattern so I am waiting sub function to finish and passing to my API but it is not working on intial load of the page?
import { subscribe } from "./pubsub";
const Test = () => {
const [productId, setProductId] = useState({});
const [response, setResponse] = useState([]);
React.useEffect(() => {
function sub() {
return new Promise((resolve, reject) => {
subscribe("product-message", (data) => {
// console.log("Got some message", data);
// setProductId(data.productId);
resolve(data.productId);
});
});
}
async function fetchData() {
let message = await sub();
let response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${message.productId}` // Here I couldn't get the async data from above useEffect
);
console.log(response);
setResponse(response);
}
fetchData();
}, []);
return <div>{response.title}</div>; //It is not printing in intial load
};
export default Test;
So here is my sandbox link: https://codesandbox.io/s/happy-forest-to9pz?file=/src/test.jsx
If you only need the response, you do not need to store productId in state and then use it in another useEffeect to fetch data. You can simply implement the logic in one useEffec. Also note that you need to use the json response from fetch call so you need to use it like
let response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${productId}`
).then(res => res.json());
or
let res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${productId}`
)
let response = await res.json();
Complete function will look like
const Test = () => {
const [response, setResponse] = useState([]);
React.useEffect(() => {
async function fetchData(productId) {
let response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${productId}`
).then(res => res.json());
console.log(response);
setResponse(response);
}
console.log("Api calls");
subscribe("product-message", (data) => {
// console.log("Got some message", data);
fetchData(data.productId);
});
}, []);
return <div>{response.title}</div>;
};
export default Test;
However if you need productId in your application, you can go via a multiple useEffect approach like you have tried in your sandbox. Also make sure that you are using thee fetch call correctly and also make sure to not make the API call wheen productId is not available
const Test = () => {
const [productId, setProductId] = useState({});
const [response, setResponse] = useState([]);
React.useEffect(() => {
console.log("Api calls");
subscribe("product-message", (data) => {
// console.log("Got some message", data);
setProductId(data.productId);
});
}, []);
React.useEffect(() => {
async function fetchData() {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${productId}` // Here I couldn't get the async data from above useEffect
);
const response = await res.json();
console.log(response);
setResponse(response);
}
if(productId) {
fetchData();
}
}, [productId]);
return <div>{response.title}</div>;
};
export default Test;
Working Sandbox
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);