useLazyQuery in Promise.all only storing last response in cache - reactjs

I'm trying to use the same query with different variables using useLazyQuery. I built a hook for this reason and it fetches everything alright. However, it never really uses cached data. I looked into the cache and it's only storing the response from the last request in the array. This is a simplified version of my hook:
const useProductSearchQuery = (categories) => {
const [getProducts] = useLazyQuery(QUERY);
const [data, setData] = useState();
useEffect(() => {
async function getProducts() {
const responses = await Promise.all(
categories.children.map((cat) =>
getProducts({ variables: { category: cat.id } })
)
);
setData(responses);
}
getProducts();
}, [productCategories, getProducts]);
return { data };
};
I'm not sure if this use case fits useLazyQuery so maybe that's why it doesn't work. I just needed an imperative way of running my queries and this seemed easier than using a consumer to pass the client around.
The other alternative would be to just iterate categories and then have a useQuery hook for each but I'd prefer having my data ready in one go.

Nevermind I didn't notice the hooks docs mention the useApolloClient hook (https://www.apollographql.com/docs/react/api/react/hooks/#useapolloclient)... Works like a charm!

Related

RTK Query response state

I'm trying to convert some Axio code to RTK query and having some trouble. The 'data' response from RTK query doesn't seem to act like useState as I thought.
Original axio code:
const [ importantData, setImportantData ] = useState('');
useEffect(() => {
async function axiosCallToFetchData() {
const response = await axiosAPI.post('/endpoint', { payload });
const { importantData } = await response.data;
setImportantData(importantData);
}
axiosCallToFetchData()
.then((res) => res)
.catch((error) => console.log(error));
}, []);
const objectThatNeedsData.data = importantData;
New RTK Query code
const { data, isSuccess } = useGetImportantDataQuery({ payload });
if(isSuccess){
setImportantData(data.importantData);
}
const objectThatNeedsData.data = importantData;
This however is giving me an infinite render loop. Also if I try to treat the 'data' object as a state object and just throw it into my component as:
const objectThatNeedsData.data = data.importantData;
Then I get an undefined error because it's trying to load the importantData before it's completed. I feel like this should be a simple fix but I'm getting stuck. I've gone through the docs but most examples just use the if statement block to check the status. The API calls are being made atleast with RTK and getting proper responses. Any advice?
Your first problem is that you always call setImportantData during render, without checking if it is necessary - and that will always cause a rerender. If you want to do that you need to check if it is even necessary:
if(isSuccess && importantData != data.importantData){
setImportantData(data.importantData);
}
But as you noticed, that is actually not necessary - there is hardly ever any need to copy stuff into local state when you already have access to it in your component.
But if accessing data.importantData, you need to check if data is there in the first place - you forgot to check for isSuccess here.
if (isSuccess) {
objectThatNeedsData.data = data.importantData;
}
All that said, if objectThatNeedsData is not a new local variable that you are declaring during this render, you probably should not just modify that during the render in general.

RTK query send data, is coming back manipulated

At work, I need to make an infinite scroll pagination with RTK-query. I am using Firestore as a DB, and I made a very basic version with just async functions super quickly. Now I need to convert what I have made to RTK query.
As I was doing this I noticed I was not longer able to fetch more data from Firestore because my query with Firestore was not responding. After doing some digging, I found out that RTK query is somehow changing my data. I will explain below with my code. This very well would be a Firestore problem as well.
async queryFn() {
try {
const { prod, lastDoc } = await fetchInitialData();
return { data: { prod, lastDoc } };
} catch (err) {
return { error: err };
}
This is how I am sending my data with RTK-query, I left a lot of code out since it is not needed. Down below is my react file, that calls my RTK query hook, as well as checks to see if the data being sent from RTK is correct
const [products, setProducts] = useState<BasicProductData[] | null>();
const [lastDocSaved, setLastDocSaved] = useState<any>();
const { data, isLoading, isError, error, isSuccess, refetch } = useFetchProductsQuery(null);
useEffect(() => {
getPost();
if (data && lastDocSaved) {
console.log(data.lastDoc === lastDocSaved);
}
}, []);
const getPost = async () => {
const { prod, lastDoc } = await fetchInitialData();
setProducts(prod);
setLastDocSaved(lastDoc);
};
As you can tell both of these ways of getting the initial data is pretty much exactly the same. Finally here is my pagination function, to fetch more data from firebase to keep the infinite scroll going.
const getMorePosts = async () => {
const postData = await fetchMoreData(lastDocSaved);
setLastDocSaved(postData.lastDoc);
setProducts([...products, ...postData.prod]);
};
I know that 'data' I get back from the RTK hook is not the same since I manually have checked if is by using that console.log in the useEffect. I also know it does not work since if I setLastDocSaved(data.lastDoc) the pagination does not work.
This is strange to me, since both data.lastDoc and const { lastDoc } = await fetchInitialData() come back as pretty much identical in the console, however lastDoc properly queries from firebase and data.lastDoc does not.

React save api requests

I am building a small online shopping web app and I have about 10 categories of products e.g. Mens, Womens, Kids etc. I want to memoize the requests. My question is how do I do it? I read about useCallback hook and I believe that I should use it here, but at the same time since there are like 10 categories how do I keep track for which category should I get from the cache and for which should I make the request? I have just considering declaring useState hooks for each category and change state for each depending on selected category.
const [productsMensCategory, setProductsMensCategory] = useState([]);
const [productsWomensCategory, setProductsWomensCategory] = useState([]);
const [productsKidsCategory, setProductsKidsCategory] = useState([]);
I have also been thinking that Redux might be useful here.
Could anyone please guide me a bit here, I would like to know how to approach this.
This is how I make a request.
const [products, setProducts] = useState([]);
useEffect(() => {
const getProducts = async (category: string) => {
try {
const response = await getCategoryProducts(category);
console.log(response);
if (response.status === 200) {
setProducts(response.data);
}
} catch (e) {
console.log(e);
}
};
getProducts(handleChangeCategory(section));
}, [section, setProducts]);
If you want these to stay saved even if you reload page you could use localStorage to make it easy. Redux would be a nice approach to not make another request, but as far as you reload it won't save state.
UseCallback only memorizes a callback, so if re-render happens this callback remains the same, it only changes if a dependency from its dependencies array changes
Good idea to have the data memoized. In my opinion, if at all you can get the data with flags based on date_modified for each category (as server response) you can then set or not set component state.
Something like below
if (response.status === 200 && category_flag_from_server !== current_category_flag_from_state) {
setProducts(response.data);
}

In React, fetch data conditional on results of an initial fetch

We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}

React useEffect infinite loop fetching data from an api

Hi I'm trying to make a twitter clone app. I am using React on the client side and Express on the server side and PostgreSQL as my database. So here's the problem, I'm trying to use the useEffect like this:
const [tweets, setTweets] = useState([]);
const getTweets = async () => {
const res = await api.get("/posts", {
headers: { token: localStorage.token },
});
setTweets(res.data);
};
useEffect(() => {
getTweets();
}, [tweets]);
I have no idea why it's looping infinite times, am I using it correctly though? I want the tweets to be updated every time I post a tweet. It's working fine but it's running infinite times. I just want it to re-render if a tweet got posted.
Here's my server code for getting all the posts:
async all(request: Request, response: Response, next: NextFunction) {
return this.postRepository.find({
relations: ["user"],
order: {
createdAt: "DESC",
},
});
}
The problem is every time you change the tweets it executes useEffect and changes the tweets and so long and so forth, so it's natural that it loops infinitely, the solution is to add a trigger that you set to true when a tweet gets posted, so the solution would be like this
const [tweets, setTweets] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const getTweets = async () => {
const res = await api.get("/posts", {
headers: { token: localStorage.token },
});
setTweets(res.data);
};
useEffect(() => {
getTweets();
setIsFetching(false);
}, [isFetching]);
and set some logic to use setIsFetching(true) in order to execute the useEffect
PS: if you use an empty array in useEffect, it would execute only when the component is mounted (at the start)
useEffect(() => {
getTweets();
}, [tweets]); // [tweets means that hook works every time 'tweets' state changes]
so your getTweets function set tweets => as tweets are changed hook works again => call getTweets => ... = infinite loop
if you want to download tweets, use empty array instead - hook will work once then
Pass empty array as a second arg for calling it once otherwise for changing it on every tweet change it will re-trigger, so whenever state will change only then it will be re-rendered like Tarukami explained. One thing you can do is check the length like mentioned below so not to compare the whole object but just the length
useEffect(() => {
getTweets();
}, [tweets.length]);
This might raise an error react-hooks/exhaustive-deps lint error (that's a bypass you can use it).
But if you want more tighter check you can compare the ids on each re-render (create a hash/key/id from all element in the array and compare them on each render) like so [tweet id here]) // Only re-subscribe if id changes

Resources