How to make React-Query return cached/previous data if network is not connected/available? - reactjs

I've modifying some existing React Native code where I've to check if network connection is available or not. If it is available, I've to fetch new data from API, otherwise I've to use cached data. This is what I've achieved so far:
export const useStudentData = (
studentId: Student['id']
): UseQueryResult<Student, Error> => {
const queryKey = studentDataKeys.list({ studentIdEq: studentData?.id });
const queryClient = useQueryClient();
const {isConnected} = useNetInfo();
if(!isConnected){
// return previously cached data here
}
const data = useQuery<Student, Error>(
studentDataKeys.detail(studentId),
async () => {
const { data: student } = await StudentDataAPI.fetchDataById(studentId);
return StudentData.deserialize({
...studentData,
assignments: studentData.assignments?.filter((assignment) => assignment.submitted)
});
},
{
staleTime: 1000 * 60 * 60 * 10,
retry: 0,
initialData: () => {
const previousStudentData = queryClient.getQueryData<Student[]>(queryKey);
return previousStudentData?.find((studentData) => studentData.id === studentId);
},
initialDataUpdatedAt: queryClient.getQueryState(queryKey)?.dataUpdatedAt,
onError() {
console.log("Error with API fetching");
}
}
);
return data;
};
How can I modify it so that if network connection is present, it should download new data otherwise return previous/old data that was cached in previous successful call?

Instead of reinventing the wheel, you can use the existing solution. That is:
import NetInfo from '#react-native-community/netinfo'
import { onlineManager } from '#tanstack/react-query'
onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => {
setOnline(!!state.isConnected)
})
})
After implementing this, react-query will automatically refetch your data when the device is back online.

You're trying to implement a feature that React-Query provides to you for free.
React-Query will keep displaying old data until new data is available. By default, React-Query will stop trying to fetch data entirely until you are back online.
Additionally, you can set the refetchOnReconnect flag to true (which is also the default) in order to request fresh data the moment you are online.

You can rely on the queryKey for that. In detail, react-query caches the result based on the key.
Your current key is studentDataKeys.detail(studentId), what about replacing it with [studentDataKeys.detail(studentId), isConnected]?

Related

How to make manual data validation/mutation work with useSWR and Axios?

I have tried everything I could so far, but can't get to make this manual data revalidation/mutation to work using const { mutate } = useSWRConfig().
I have tried setting GlobalConfig on my app, and not, but to no changes.
For example: In my "AddressesList" component, I fetch addresses list using the hook:
const { data, isValidating, error} = useSWR('v1/addresses', fetchMyAddresses, {
revalidateOnFocus: false,
revalidateIfStale: false,
});
where the fetcher is a simple is a simple fetchMyAddresses = () => axiosApiInstance.get('v1/addresses').then(res => res.data);
Then in another component, after creating a new address, I use:
const { mutate } = useSWRConfig();
...
await createNewAddress();
if (success) {
mutate('v1/addresses');
redirect('AddressesList');
}
and on success, I am redirected to the AddressesList. But my data list is not updated with the new address... how is that supposed to work?

react-query always return stale data and no call is made to server

I recently started using react-query and have encountered the issue that always stale data is returned and no call to server is made. here is the react query related code:
export function useGetAccount(id: number){
return useQuery([`account${id}`, id], async (args) => {
const [key, accountId] = args.queryKey
const [acc, teams, modules] = await Promise.all([
getAccount(),
getTeams(),
getModules()])
let account: AccountDetail = {
accountId: acc.accountId,
userId: acc.userId,
companyId: acc.companyId,
login: acc.login,
email: acc.email,
description: acc.description,
isActive: acc.isActive,
providers: acc.providers,
teams: teams,
modules: modules
}
return account
async function getAccount() {
const api = createApi() // <= axios wrapper
const { data } = await api.get(`accounts/${accountId}`, undefined, undefined)
return data as AccountModel
}
async function getTeams() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/teams`, undefined, undefined)
const { collection } = data as ResponseCollectionType<AccountTeam>
return collection
}
async function getModules() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/resources`, undefined, undefined)
const { collection } = data as ResponseCollectionType<ModuleAccessModel>
return collection
}
})
}
I even reduced the cache time but still to no avail. I do not see any calls made to server side except after a long delay or if I open the browser in incognito mode then first time the data is fetched and then no call is made.
this is used in a component which shows the details and is passed the id as a prop. everything is working fine except that the data is the one which was retrieved first time and even a refresh (F5) returns the stale data.
what changes do I need to make in this case?
[observation]: Ok, it does make a call but only after exact 5 minutes.
well the problem is not in react-query but in axios, described here Using JavaScript Axios/Fetch. Can you disable browser cache?
I used the same solution i.e. appending timestamp to the requests made by axios and everything worked fine.

React-query: how to update the cache?

I have a very basic app that fetches a user and allows to change his name. I fetch the user with React query so I can benefit from the cache feature. It works.
However, when I want to update the user, I use a classic post request with axios. Once the user is updated in the database, I need to update the cache directly in the updateUser() function. I've seen tutorials on the web that use queryCache.setCache, but it doesn't work here. How to fix this? Or maybe there is a better way to handle such queries?
Also, I notice a huge number of rendering... (see the "user render" console.log in the app file).
For the convenience, I've made a small script on a codesandbox with the pokeapi:
https://codesandbox.io/s/api-service-syxl8?file=/src/App.js:295-306
Any help would be greatly appreciated!
So, I'll show you what I do:
const updateUser = async (userUpdates: User) => {
const data = await UserService.updateUser(userUpdates); // return axios data
return data;
}
// if you want optimistic updating:
const { mutate: mutateUser } = useMutation(updateUser, {
onMutate: async (userUpdates) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['user', userUpdates.id]);
// Snapshot the previous value
const previousUser = queryClient.getQueryData(['user', userUpdates.id]);
// Optimistically update to the new value
queryClient.setQueryData(['user', userUpdates.id], userUpdates);
// Return a context with the previous user and updated user
return { previousUser, userUpdates }; // context
},
// If the mutation fails, use the context we returned above
onError: (err, userUpdates, context) => {
queryClient.setQueryData(['user', context.userUpdates.id], context.previousUser);
},
// Always refetch after error or success:
onSettled: (userUpdates) => {
queryClient.invalidateQueries(['user', userUpdates.id]);
}
});
// then to update the user
const handleUpdateUser = (userUpdates: User) => mutateUser(userUpdates);
This all comes from the docs:
Optimistic Updates

next.js swr I do not want to execute it only at the time of initial drawing

Development Enviroment
・ next.js
・ typescript
・ swr
it uses swr to communicate with swr.
I want to run it only when the value of the query is changed.
but is also executed during the initial drawing.
what can I do to prevent it from running during the initial drawing?
export const useUserQuery = (query: string) => {
const fetcher = (url) => {
Axios.get(url).then((res) => {
return res;
});
const path = `users/?query=${query}`;
const { data, error } = useSWR<IUser>(path);
return {
data: data,
loading: !error && !data,
Error: error,
};
};
};
UPDATED:
use initalData and disable revalidateOnMount then it should work as you expect.
see more in the docs: https://github.com/vercel/swr
It seems that you what to refeatch on variable change.
If you try this, it won't work, because even if token changes, SWR will still use the same key and return the wrong data.
useSWR('/api/user', url => fetchWithToken(url, token))
Instead, you can use an array as the key parameter, which contains multiple arguments of fetcher:
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
See more in docs

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>
)
}

Resources