React Query: rerun onSuccess method of a cached query - reactjs

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.

Related

ReactJS delay update in useState from axios response

I am new to react js and I am having a hard time figuring out how to prevent delay updating of use state from axios response
Here's my code:
First, I declared countUsername as useState
const [countUsername, setUsername] = useState(0);
Second, I created arrow function checking if the username is still available
const checkUser = () => {
RestaurantDataService.checkUsername(user.username)
.then(response => {
setUsername(response.data.length);
})
.catch(e => {
console.log(e);
})
}
So, every time I check the value of countUsername, it has delay like if I trigger the button and run checkUser(), the latest response.data.length won't save.
Scenario if I console.log() countUseranme
I entered username1(not available), the value of countUsername is still 0 because it has default value of 0 then when I trigger the function once again, then that will just be the time that the value will be replaced.
const saveUser = () => {
checkUser();
console.log(countUsername);
}
Is there anything that I have forgot to consider? Thank you
usually there is a delay for every api call, so for that you can consider an state like below:
const [loading,toggleLoading] = useState(false)
beside that you can change arrow function to be async like below:
const checking = async ()=>{
toggleLoading(true);
const res = await RestaurantDataService.checkUsername(user.username);
setUsername(response.data.length);
toggleLoading(false);
}
in the above function you can toggle loading state for spceifing checking state and disable button during that or shwoing spinner in it:
<button onClick={checking } disabled={loading}>Go
i hope this help
.then is not synchronous, it's more of a callback and will get called later when the api finishes. So your console log actually goes first most of the time before the state actually saves. That's not really something you control.
You can do an async / await and return the data if you need to use it right away before the state changes. And I believe the way state works is that it happens after the execution:
"State Updates May Be Asynchronous" so you can't really control when to use it because you can't make it wait.
In my experience you use the data right away from the service and update the state or create a useEffect, i.g., useEffect(() => {}, [user]), to update the page with state.
const checkUser = async () => {
try {
return await RestaurantDataService.checkUsername(user.username);
} catch(e) {
console.log(e);
}
}
const saveUser = async () => {
const user = await checkUser();
// do whatever you want with user
console.log(user);
}

ReactQuery make refetch with old data

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.

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.

How to setup a function which gets app settings and sets it as localStorage before the page loads. (next.js)

I've been working on a Next.JS web application for the past couple of days but I've reached a problem. The app has an API call (/api/settings) which returns some settings about the application from the database. Currently, I have a function which returns these settings and access to the first component:
App.getInitialProps = async () => {
const settingsRequest = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/settings`
);
const settingsResponse = await settingsRequest.json();
return { settings: settingsResponse };
};
This does work and I am able to pass in settings to components but there are two problems with this:
I need to nest the prop through many components to reach the components that I need
This request runs every time a page is reloaded/changed
Essentially, I need to create a system that does this:
runs a function in the _app.tsx getInitialProps to check if the data is already in localStorage, if not make the API request and update localStorage
have the localStorage value accessible from a custom hook.
Right now the problem with this is that I do not have access to localStorage from the app.tsx getInitialProps. So if anyone has an alternative to run this function before any of the page loads, please let me know.
Thanks!
I found a solution, it might be a janky solution but I managed to get it working and it might be useful for people trying to achieve something similar:
First we need to create a "manager" for the settings:
export const checkIfSettingsArePresent = () => {
const settings = localStorage.getItem("app_settings");
if (settings) return true;
return false;
};
export const getDataAndUpdateLocalStorage = async () => {
const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/settings`);
const response = await r.json();
localStorage.setItem("app_settings", JSON.stringify(response));
};
With that created we can add a UseEffect hook combined with a useState hook that runs our function.
const [doneFirst, setDoneFirst] = useState<boolean>(false);
useEffect(() => {
const settingsPreset = checkIfSettingsArePresent();
if (performance.navigation.type != 1)
if (settingsPreset) return setDoneFirst(true);
const getData = async () => {
await getDataAndUpdateLocalStorage();
setDoneFirst(true);
};
getData();
}, []);
//any other logic
if (!doneFirst) {
return null;
}
The final if statement makes sure to not run anything else before the function.
Now, whenever you hot-reload the page, you will see that the localStorage app_settings is updated/created with the values from the API.
However, to access this more simply from other parts of the app, I created a hook:
import { SettingsType } from "#sharex-server/common";
export default function useSettings() {
const settings = localStorage.getItem("app_settings") || {
name: "ShareX Media Server",
};
//#ts-ignore
return JSON.parse(settings) as SettingsType;
}
Now I can import useSettings from any function and have access to my settings.

Ho to wait with fetch until redux state is available?

I want to fetch some data from a database, and depending on the user the returned data should differ. The way i tried it, was by passing the userid as a query. The id is stored as a redux state. The problem is, that it takes some time before the redux state is available. Ive tried fixing this with if statements, and rerunning the useEffect everytime the auth state is updated. This doesn't work.
I want to fetch, when the redux state auth.user.id is available. Which it is like .1 sec after the initial load.
Here is my code:
const auth = useSelector((state) => state.auth);
useEffect(async () => {
if (auth.token.length > 0) {
const res = await getData(`calendar/?userId=${auth.user.id}`);
setLessons(res.lessons);
setEvents(res.events);
}
}, [auth, date]);
I believe useEffect is already asynchronous, so you don't need to use the async keyword in the anonymous callback. You can create the async function for that logic elsewhere and call it within the useEffect.
Similarly, you could put in self calling async function within your useEffect as such:
useEffect(() => {
(async () => {
if (auth.token.length) {
try {
const res = await getData(`calendar/?userId=${auth.user.id}`);
setLessons(res.lessons);
setEvents(res.events);
}catch (err) {console.log(err);}
}
})();
}, [auth, date]);
I think this link may be helpful:
React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing
So with the basic understanding, I assume that you need to call the API whenever userId is available. try the below useEffect
useEffect(async () => {
// check user id is available here
if (auth.user && auth.user.id) {
const res = await getData(`calendar/?userId=${auth.user.id}`);
setLessons(res.lessons);
setEvents(res.events);
// some other statements
}
}, [auth, date]);

Resources