ReactQuery make refetch with old data - reactjs

I use ReactQuery in My App. I create a custom hook with the state. I export function that changes this state in my application. I expect that after changing state I will make a re-fetch with a new state. But it does not happen. I add UseEffect on change state and execute refetch() after my state is changed, but it fetches with previous data, with a body before I updated the state.
So inside UseEffect I see that body is changed(with new data) -> call refetch -> useQuery call ()=>fetchCommissionsData(body), but with old body(not new data)
I'm using: "react": "^18.1.0", "react-query": "^3.39.1"
import React, { useState, useEffect } from 'react'
import { getCommissionsData, getSummary } from "../utils/apis";
import { queryKeys } from "../utils/constants";
import { useMutation, useQueryClient, useQuery } from "react-query";
//REST API
const fetchCommissionsData = async (request) => {
const response = await getCommissionsData(request)
return response
}
export function useCommissionsData() {
const [body, setBody] = useState(null)
const queryClient = useQueryClient()
const searchValues = queryClient.getQueryData(queryKeys.searchValues)
//get data from client,add some extra parameters and update state with request body
const getCommissionsData = (data) => {
const body = {
...data,
movementType: searchValues.movementType,
viewType: searchValues.viewType,
summaryType: searchValues.summaryType
}
setBody(body)
}
//Effect: refetch query to get new commissions data on every change with request body
useEffect(() => {
refetch()
}, [body])
//default data is empty array
const fallback = [];
const { data = fallback, refetch } = useQuery([queryKeys.commissionsData], () => fetchCommissionsData(body), {
refetchOnWindowFocus: false,
enabled: false // turned off by default, manual refetch is needed
});
return { data, getCommissionsData }

The misconception here is that you're thinking in an imperative way: "I have to call refetch if something changes, so I need an effect for that."
react-query however is quite declarative, so you don't need any of that. refetch is meant for refetching with the same parameters. That's why it also doesn't accept any variables as input.
Judging from your code, you want to fetch once body is set. This is best done by:
adding body to the queryKey
disabling the query only when body is null:
const [body, setBody] = useState(null)
const { data = fallback } = useQuery(
[queryKeys.commissionsData, body],
() => fetchCommissionsData(body),
{
enabled: body === null
}
);
Then, all you need to do is call setBody with a new value, and react-query will automatically trigger a refetch with the new body values. Also, it will cache per body, so if you go back from one filter to the other, you will still have cached data.

Related

React Query: rerun onSuccess method of a cached query

What I have
I am getting the user's location (latitude/longitude) which I use to call a google geocode API, unless the user's coords change, the request is not running again, since the query it uses the user's coords as queryKey array dependecy.
The problem
the problem is that I'm running some operations in the onSuccess query method, this method is only run when any of the queryKey dependencies change, and I mentioned this not happen.
How to run the onSuccess method whether the queryKey dependencies change or not?
Reference code
export const useGoogleReverseGeocoding = (coords) => {
const url = 'someUrl';
const request = createClient(); // axios abstraction
return useQuery({
queryKey: ['google-geocode', coords],
queryFn: request,
enabled: !!coords,
onSuccess: (data) => {
const searchTerm = removeGlobalCodeText(data?.plus_code?.compound_code);
// set searchterm in a global store. This searchterm change with
// different user actions, so if the user re-share his location
// I need to run onSuccess transformation again.
setSearchTerm(searchTerm);
},
});
};
As I was explaining in my comment, onSuccess can't be fired without the query itself firing again. Since certain user actions should trigger the transformations on onSuccess, you have a couple of ways to go about this, one of them would be to move these transformations on a useEffect hook and add some user action related flag on the dependencies array. The other proposed solution would be to invalidate the query upon these user actions, so it will be refetched and the transformations on onSuccess will execute.
You can achieve this using useQueryClient hook which returns the current QueryClient instance. You can invalidate the query from anywhere as long as the component is wrapped by QueryClientProvider. For this example and for convenience, I will include this hook on useGoogleReverseGeocoding custom hook.
Example:
Custom hook:
export const useGoogleReverseGeocoding = (coords) => {
const queryClient = useQueryClient()
const url = 'someUrl';
const request = createClient(); // axios abstraction
const geocodingData = useQuery({
queryKey: ['google-geocode', coords],
queryFn: request,
enabled: !!coords,
onSuccess: (data) => {
const searchTerm = removeGlobalCodeText(data?.plus_code?.compound_code);
// set searchterm in a global store. This searchterm change with
// different user actions, so if the user re-share his location
// I need to run onSuccess transformation again.
setSearchTerm(searchTerm);
},
});
const invalidateQueryOnAction = () => queryClient.invalidateQueries(['google-geocode'])
return { geocodingData, invalidateQueryOnAction }
};
Some component:
const dummyCoords = {
lat: 33.748997,
lng: -84.387985
}
const SomeComponent = () => {
const { geocodingData, invalidateQueryOnAction } =
useGoogleReverseGeocoding(dummyCoords)
const handleSomeUserAction = () => {
// handle action...
// Invalidate query, so the query gets refetched and onSuccess callback executes again
invalidateQueryOnAction()
}
}
PS: If #TkDodo comes along with a different solution for this, I would suggest to go for it instead.

How to refresh graphql data on state change

import { useQuery, gql, useMutation } from "#apollo/client";
const Questions = () => {
const [modal, setModal] = useState(false)
const QUESTION_QUERIES = gql`
query getQuestions(
$subjectRef: ID
$gradeRef: ID
$chapterRef: ID
$status: String
) {
getQuestions(
subjectRef: $subjectRef
gradeRef: $gradeRef
chapterRef: $chapterRef
status: $status
) {
id
question_info
question_type
answer
level
published
subjectRef
gradeRef
chapterRef
levelRef
streamRef
curriculumRef
options
status
subject
grade
chapter
stream
curriculum
}
}
`;
const { loading, error, data } = useQuery(QUESTION_QUERIES);
return (
<div>
</div>
)
}
Here is my react graphql code.
I wants to fetch data when modal change using state if modal status change to true to false or false to
true it will make api call to fetch questions again
Please take a look how to solve the issue.
use useLazyQuery:
const [updateFn,{ loading, error, data }]= useLazyQuery(QUESTION_QUERIES);.
Then create useEffect with modal as dependency variable, and call updateFn inside useEffect
You want to fetch data after the modal state change, So you simply use useEffect and put modal in the dependency list of the useEffect and for useQuery there is also a function called refetch, the logic would be like this
const { loading, error, data, refetch } = useQuery(QUESTION_QUERIES);
useEffect(() => {
// the reason I put if condition here is that this useEffect will
// also run after the first rendering screen so you need to put a check
// to do not run refetch in that condition
if (data) refetch();
}, [modal]);

Prevent useSWR from fetching until mutate() is called

I have a React application which uses SWR + Axios for data fetching (https://swr.vercel.app/docs/data-fetching). The issue is that my custom hook which uses useSwr is fetching all data initially whenever the hook is initialized. My goal is to fetch only when I call mutate. Currently the initial fetch is happening even without calling mutate. Any suggestion on how to achieve my goal here?
My application is wrapped in SWRConfig:
<SWRConfig
value={{
fetcher,
}}
>
<App/>
</SWRConfig>
The fetcher is described as so:
const dataFetch = (url) => axios.get(url).then((res) => res.data);
function fetcher(...urls: string[]) {
if (urls.length > 1) {
return Promise.all(urls.map(dataFetch));
}
return dataFetch(urls);
}
My custom hook using useSwr
import useSWR, { useSWRConfig } from "swr";
export function useCars(registrationPlates: number[]): ICars {
const { mutate } = useSWRConfig();
const { data: carData} = useSWR<Car[]>(
carsToFetchUrls(registrationPlates), // returns string array with urls to fetch
{
revalidateOnFocus: false,
revalidateOnMount: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
}
);
const getCar = (
carRegistrationPlate: number,
): Car => {
console.log(carData) // carData contains data from fetch even before calling mutate()
void mutate();
...
}
Usage: (this will be located in some component that wants to use the useCars hook)
const { getCar } = useCars(carsRegistrationPlates);
You can use conditional fetching in the useSWR call to prevent it from making a request.
From useSWR Conditional Fetching docs:
Use null or pass a function as key to conditionally fetch data. If the
function throws or returns a falsy value, SWR will not start the
request.
export function useCars(registrationPlates: number[], shouldFetch): ICars {
const { data: carData} = useSWR<Car[]>(
shouldFetch ? carsToFetchUrls(registrationPlates) : null,
{ // Options here }
);
// ...
return { carData, /**/ }
}
You can then use it as follows to avoid making the initial request.
const [shouldFetch, setShouldFetch] = useState(false);
const { carData } = useCars(carsRegistrationPlates, shouldFetch);
Then, when you want the make the request simply set shouldFetch to true.
setShouldFetch(true)
Here's a possible way of implementing what you are hoping to achieve.
I've used a similar approach in one of my production app.
Start by creating a custom swr hook as so
const useCars = (registrationPlates: number[]) => {
const fetcher = (_: string) => {
console.log("swr-key=", _);
return dataFetch(registrationPlates);
};
const { data, error, isValidating, revalidate, mutate } = useSWR(`api/car/registration/${JSON.stringify(registrationPlates)}`, fetcher, {
revalidateOnFocus: false,
});
return {
data,
error,
isLoading: !data && !error,
isValidating,
revalidate,
mutate,
};
};
export { useCars };
Now, you can call this hook from any other component as
const { data, error, isLoading, isValidating, revalidate, mutate } = useCars(carsRegistrationPlates);
You now control what you want returned by what you pass to useCars above.
Notice what is passed to the first argument to useSwr in our custom swr hook, this is the key swr uses to cache values and if this remains unchanged then swr will transparently returned the cached value.
Also, with this custom hook you are getting states such as loading, error etc. so you can take appropriate action for each of these states in your consuming component.

refetch in reactQuery is not return the data

I am using reactQuery in my react application. I need to call one get API in button click. for that i am using refetch option in reactQuery. API call is working fine but my response data is coming undefined. I checked in browser network there i can see the response.
My API call using reactQuery
const { data: roles, refetch: roleRefetch } = useQuery('getRoles', () => api.getRoles('ID_234'), { enabled: false });
My click event
const handleAdd = (e) => { roleRefetch(); console.log(roles) }
My action call using axios
export const getRoles = (name) => axios.get(roles/list?sa_id=${name}, { headers: setHeader }).then(res => res);
const handleAdd = (e) => { roleRefetch(); console.log(roles) }
this not how react works, and it's not react-query specific. calling a function that updates some state will not have your state be available in the next line. It will make it available in the next render cycle. Conceptually, you want this to work, which cannot with how react is designed:
const [state, setState] = React.useState(0)
<button onClick={() => {
setState(1)
console.log(state)
}}
here, the log statement will log 0, not 1, because the update doesn't happen immediately, and this is totally expected.
With react-query, what you can do is await the refetch, because its async, and it will give you the result back:
const handleAdd = async (e) => {
const { data } = await roleRefetch();
console.log(data)
}
or, depending on what you actually want to do, you can:
use data in the render function to render something - it will always be up-to-date.
use theonSuccess callback of useQuery to trigger side-effects whenever data is fetched
spawn a useEffect in the render function that does the logging:
const { data: roles, refetch: roleRefetch } = useQuery('getRoles', () => api.getRoles('ID_234'), { enabled: false });
React.useEffect(() => {
console.log(roles)
}, [roles])
on a more general note, I think disabling a query and then calling refetch on a button click is very likely not idiomatic react-query. Usually, you have some local state that drives the query. in your case, that's likely the id. Dependencies of the query should go to the queryKey, and react-query will trigger a refetch automatically when the key changes. This will also give you caching by id. You can use enabled to defer querying when your dependencies are not yet ready. Here's what I would likely do:
const [id, setId] = React.useState(undefined)
const { data: roles } = useQuery(['getRoles', id], () => api.getRoles(id), { enabled: !!id });
const handleAdd = (e) => { setId('ID_234') }
of course, id doesn't have to come from local state - it could be some other form of client state as well, e.g. a more global one.

Not awaiting for data in useEffect

I have a useEffect in my component that is waiting for data from the context so that it can set it in state. But its not waiting for the state and is moving on to the next line of code to set isLoading to false.
I'd like it to wait for the data so that I can render the loading.
I tried setting the isFetchingData in the context but I had run into problems where if another component calls it first it would set the isFetchingData state to false.
First call to ReactContext is setting the isLoading sate to false
It is fine for results to come back with no records. The component would render 'No records found'. Therefore, I cannot check the length on state to say if length is zero then keep loading.
Following is my code:
Context
const [activeEmployees, setActiveEmployees] = useState([]);
const [terminatedEmployees, setTerminatedEmployees] = useState([]);
useEffect(() => {
getEmployees()
.then(response => {
/// some code...
setActiveEmployees(response.activeEmployees)
setTerminatedEmployees(response.terminatedEmployees)
});
});
Component
const EmployeesTab = () => {
const { activeEmployees, terminatedEmployees } = useContext(BlipContext);
//Component states
const [isFetchingData, setIsFetchingData] = useState(true);
const [newEmployees, setNewEmployees] = useState([]);
const [oldEmployees, setOldEmployees] = useState([]);
useEffect(() => {
async function getData() {
await setNewEmployees(activeEmployees);
await setOldEmployees(terminatedEmployees);
setIsFetchingData(false);
}
getData();
}, [activeEmployees, terminatedEmployees, isFetchingData]);
if(isFetchingData) {
return <p>'Loading'</p>;
}
return (
// if data is loaded render this
);
};
export default EmployeesTab;
Since you have useState inside your useContext, I don't see the point of storing yet again the activeEmployees in another state.
If you want to have a local loading variable it could something like:
const loading = !(activeEmployees.length && terminatedEmployees.length);
This loading will update whenever getEmployees changes.
And to answer you question, the reason await is not having an effect is because setState is synchronous.

Resources