React-Query: How to useQuery when button is clicked - reactjs

I am new to this react-query library.
I know that when I want to fetch data, with this library I can do something like this:
const fetchData = async()=>{...}
// it starts fetching data from backend with this line of code
const {status, data, error} = useQuery(myKey, fetchData());
It works. But how to trigger the data fetching only when a button is clicked? , I know I probably could do something like <Button onPress={() => {useQuery(myKey, fetchData())}}/> , but how to manage the returned data and status...

According to the API Reference, you need to change the enabled option to false to disable a query from automatically running. Then you refetch manually.
// emulates a fetch (useQuery expects a Promise)
const emulateFetch = _ => {
return new Promise(resolve => {
resolve([{ data: "ok" }]);
});
};
const handleClick = () => {
// manually refetch
refetch();
};
const { data, refetch } = useQuery("my_key", emulateFetch, {
refetchOnWindowFocus: false,
enabled: false // disable this query from automatically running
});
return (
<div>
<button onClick={handleClick}>Click me</button>
{JSON.stringify(data)}
</div>
);
Working sandbox here
 
Bonus: you can pass anything that returns a boolean to enabled.
That way you could create Dependant/Serial queries.
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', user.id],
getProjectsByUser,
{
// `user` would be `null` at first (falsy),
// so the query will not execute until the user exists
enabled: user,
}
)

You have to pass the manual: true parameter option so the query doesn't fetch on mount. Also, you should pass fetchData without the parentheses, so you pass the function reference and not the value.
To call the query you use refetch().
const {status, data, error, refetch} = useQuery(myKey, fetchData, {
manual: true,
});
const onClick = () => { refetch() }
Refer to the manual querying section on the react-query docs for more info
https://github.com/tannerlinsley/react-query#manual-querying

Looks like the documentation changed and is missing the manual querying section right now. Looking at the useQuery API however, you'd probably need to set enabled to false, and then use refetch to manually query when the button is pressed. You also might want to use force: true to have it query regardless of data freshness.

You can try this version:
const fetchData = async()=>{...}
// it starts fetching data from backend with this line of code
const {status, data, error, refetch } = useQuery(
myKey,
fetchData(),
{
enabled: false,
}
);
const onClick = () => { refetch() }
// then use onClick where you need it
From documentation Doc:
enabled: boolean
Set this to false to disable this query from automatically running.
Can be used for Dependent Queries.
refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>
A function to manually refetch the query.
If the query errors, the error will only be logged. If you want an error to be thrown, pass the throwOnError: true option
If cancelRefetch is true, then the current request will be cancelled before a new request is made

There is another way to do this that also works if you want to trigger multiple refetches.
const [fetch, setFetch] = useState(null);
const query = useQuery(["endpoint", fetch], fetchData);
const refetch = () => setFetch(Date.now());
// call the refetch when handling click.
If you want to refetch multiple entities you could have a top level useState that is called for instance fetchAll and:
...
const query = useQuery(["endpoint", fetch, fetchAll], fetchData);
...
and this code will also trigger if you press a button to fetch all.

At first react query gives us enabled option and by default it is true
const fetchData = async()=>{...}
const {status, data, error , refetch} = useQuery(myKey, fetchData() , {
enabled : false
}
);
<button onClick={() => refetch()}>Refetch</button>

If the key is the same, then use refetch(), if the key is different then use useState to trigger the query.
For example:
const [productId, setProductId] = useState<string>('')
const {status, data, error, refetch} = useQuery(productId, fetchData, {
enable: !!productId,
});
const onClick = (id) => {
if(productId === id) {
refetch()
}
else {
setProductId(id)
}
}

you can use useLazyQuery()
import React from 'react';
import { useLazyQuery } from '#apollo/client';
function DelayedQuery() {
const [getDog, { loading, error, data }] = useLazyQuery(GET_DOG_PHOTO);
if (loading) return <p>Loading ...</p>;
if (error) return `Error! ${error}`;
return (
<div>
{data?.dog && <img src={data.dog.displayImage} />}
<button onClick={() => getDog({ variables: { breed: 'bulldog' } })}>Click me!</button>
</div>
);
}
reference: https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery

Related

Refetching queries does not work - React Query

I'm trying to refetch some queries after one success but it's not working!
I used two ways to handle it by using refetchQueries() / invalidateQueries()
1- onSuccess callback
export const useMutateAcceptedOrder = () => {
const queryClient = useQueryClient();
return useMutation(
['AcceptedOrder'],
(bodyQuery: AcceptedOrderProps) => acceptOrder(bodyQuery),
{
onSuccess: () => {
console.log('success, refetch now!');
queryClient.invalidateQueries(['getNewOrders']); // not work
queryClient.refetchQueries(['getNewOrders']); // not work
},
onError: () => {
console.error('err');
queryClient.invalidateQueries(['getNewOrders']); // not work
},
},
);
};
second way
const {mutateAsync: onAcceptOrder, isLoading} = useMutateAcceptedOrder();
const acceptOrder = async (orderId: string) => {
const body = {
device: 'iPhone',
version: '1.0.0',
location_lat: '10.10',
location_lng: '10.10',
orderId: orderId,
os: Platform.OS,
source: 'mobile',
token: userInfo.token,
};
await onAcceptOrder(body);
queryClient.refetchQueries(['getNewOrders']); // not work
queryClient.invalidateQueries(['getActiveOrders']); // not work
handleClosetModalPress();
};
sample of query I wanted to refetch after the success
export const useNewOrders = (bodyQuery: {token: string | null}) => {
console.log('token>>', bodyQuery.token);
return useQuery(['getNewOrders'], () => getNewOrders(bodyQuery),
{
enabled: bodyQuery.token != null,
});
};
App.tsx
const App: React.FC<AppProps> = ({}) => {
const queryClient = new QueryClient();
if (__DEV__) {
import('react-query-native-devtools').then(({addPlugin}) => {
console.log('addPlugin');
addPlugin({queryClient});
});
}
useEffect(() => {
RNBootSplash.hide({fade: true}); // fade
}, []);
return (
<GestureHandlerRootView style={{flex: 1}}>
<QueryClientProvider client={queryClient}>
<BottomSheetModalProvider>
<AppContainer />
</BottomSheetModalProvider>
</QueryClientProvider>
</GestureHandlerRootView>
);
};
export default App;
--
EDIT
So after using the react-query-native-devtools Debug tool, I can't see any query in the first tab recorded in the debugger! Although the data fetched well.
So I guess that's why the refetch did not work in this case!
Any query in the first tab I can't refetch it again
Steps to reproduce:
open App - Active Tab (first tab)
check the status of the queries
nothing recorded in the debugger
Navigate to any other screen/tab
Check the status of queries
all screen queries recorded in the debugger
Per https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable:
If you move the client creation into the App component, and your component re-renders for some other reason (e.g. a route change), your cache will be thrown away:
Need to init queryClient like that:
const [queryClient] = React.useState(() => new QueryClient())
I was facing the same issue and haven't been able to find a proper solution for this, however I have found a hacky workaround. You just call your refetch function or invalidate queries inside a setTimeout.

How to show loader after updating data by queryClient.invalidateQueries?

In my project, I am trying to redirect to the listed page after updating an item. The code is working properly but here I am facing an issue, the loader is not working.
export const useUpdateStatusArchiveSurvey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateArchiveSurvey,
onSuccess: () => {
queryClient.invalidateQueries(['searched-public-survey']);
},
});
};
By using "invalidateQueries" the updated values are displayed in the list but the loader is not working.
...
...
const {
data: queriedSurvey,
fetchNextPage: fetchNextQueries,
isLoading,
} = useListAllPublicSurvey({
query: search,
status: tab,
orderDesc: orderDesc,
actionPlanId: actionValue?.id,
});
useEffect(() => {
fetchNextQueries();
}, [queriedSurvey, search, tab, orderDesc, actionValue]);
const querySurvey = useMemo(
() =>
queriedSurvey?.pages
.map((page) => page.edges.map((edge: object) => edge))
.flat(),
[queriedSurvey, search]
);
...
...
const queryPlans = useMemo(
() =>
queriedPlans?.pages
.map((page) => page.edges.map((edge: object) => edge))
.flat(),
[queriedPlans, actionSearch]
);
const onChange = (e: any) => {
setActionValue(e);
};
console.log("isLoading", isLoading);
if (isLoading) {
return <Glimmer open={isLoading} />;
}
return (
....
....
when I console the "isLoading" at the initial call it is "true" otherwise it is "false" always.
React-query has several flags in the object returned by the useQuery hook. Note that isLoading will only be true if there is no data and the query is currently fetching. Since you already have data and you invalidated it, the stale data will be present until the refetch is complete. Use the isFetching flag to determine if a fetching is in progress regardless of having stale data or not.

React Query useMutation set mutationKey dynamically

In my React project using React Query, I have a functional component MoveKeywordModal such that:
when it first loads, it fetches from API endpoint api/keyword_lists to fetch a bunch of keywordLists data. For each of these keywordLists, call it list, I create a clickable element.
When the clickable element (wrapped in a HoverWrapper) gets clicked, I want to send a POST API request to api/keyword_lists/:list_id/keyword_list_items/import with some data.
where :list_id is the id of the list just clicked.
export const MoveKeywordModal = ({
setShowMoveKeywordModal,
keywordsToMove
}) => {
const { data: keywordLists } = useQuery('api/keyword_lists', {})
const [newKeywordList, setNewKeywordList] = useState({})
const { mutate: moveKeywordsToList } = useMutation(
`api/keyword_lists/${newKeywordList.id}/keyword_list_items/import`,
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
const availableKeywordLists = keywordLists
.filter(l => l.id !== activeKeywordList.id)
.map(list => (
<HoverWrapper
id={list.id}
onClick={() => {
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList, data: keywordsToMove }
})
}}>
<p>{list.name}</p>
</HoverWrapper>
))
return (
<>
<StyledModal
isVisible
handleBackdropClick={() => setShowMoveKeywordModal(false)}>
<div>{availableKeywordLists}</div>
</StyledModal>
</>
)
}
Despite calling setNewKeywordList(list) in the onClick of the HoverWrapper, it seems the newKeywordList.id is still not defined, not even newKeywordList is defined.
What should I do to fix it?
Thanks!
react doesn’t perform state updates immediately when you call the setter of useState - an update is merely 'scheduled'. So even though you call setNewKeywordList, the newKeywordList will not have the new value in the next line of code - only in the next render cycle.
So while you are in your event handler, you’ll have to use the list variable:
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList: list, data: keywordsToMove }
})
/edit: I just realized that your call to useMutation is not correct. It doesn’t have a key like useQuery, it has to provide a function as the first argument that takes variables, known as the mutation function:
const { mutate: moveKeywordsToList } = useMutation(
(variables) => axios.post(`api/keyword_lists/${variables.newKeywordList.id}/keyword_list_items/import`),
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
see also: https://react-query.tanstack.com/guides/mutations

Mocking the fetching state of URQL in Jest

I'm trying to mock the fetching state in my tests. The state with data and state with an error were successfully mocked. Below you can find an example:
const createMenuWithGraphQL = (graphqlData: MockGraphQLResponse): [JSX.Element, MockGraphQLClient] => {
const mockGraphQLClient = {
executeQuery: jest.fn(() => fromValue(graphqlData)),
executeMutation: jest.fn(() => never),
executeSubscription: jest.fn(() => never),
};
return [
<GraphQLProvider key="provider" value={mockGraphQLClient}>
<Menu {...menuConfig} />
</GraphQLProvider>,
mockGraphQLClient,
];
};
it('displays submenu with section in loading state after clicking on an item with children', async () => {
const [Component, client] = createMenuWithGraphQL({
fetching: true,
error: false,
});
const { container } = render(Component);
const itemConfig = menuConfig.links[0];
fireEvent.click(screen.getByText(label));
await waitFor(() => {
const alertItem = screen.getByRole('alert');
expect(client.executeQuery).toBeCalledTimes(1);
expect(container).toContainElement(alertItem);
expect(alertItem).toHaveAttribute('aria-busy', 'false');
});
});
The component looks like the following:
export const Component: FC<Props> = ({ label, identifier }) => {
const [result] = useQuery({ query });
return (
<div role="group">
{result.fetching && (
<p role="alert" aria-busy={true}>
Loading
</p>
)}
{result.error && (
<p role="alert" aria-busy={false}>
Error
</p>
)}
{!result.fetching && !result.error && <p>Has data</p>}
</div>
);
};
I have no idea what am I doing wrong. When I run the test it says the fetching is false. Any help would be appreciated.
maintainer here,
I think the issue here is that you are synchronously returning in executeQuery. Let's look at what happens.
Your component mounts and checks whether or not there is synchronous state in cache.
This is the case since we only do executeQuery: jest.fn(() => fromValue(graphqlData)),
We can see that this actually comes down to the same result as if the result would just come out of cache (we never need to hit the API so we never hit fetching).
We could solve this by adding a setTimeout, ... but Wonka (the streaming library used by urql) has built-in solutions for this.
We can utilise the delay operator to simulate a fetching state:
const mockGraphQLClient = {
executeQuery: jest.fn(() => pipe(fromValue(data), delay(100)),
};
Now we can check the fetching state correctly in the first 100ms and after that we are able to utilise waitFor to check the subsequent state.

How can I update a component's state based on its current state with React Hooks?

In my component, I'm running a function that iterates through keys in state and updates properties as async functions complete. However, it looks like it's updating the state to the state as it existed prior to the function running.
This is the code for my component:
interface VideoDownloaderProps {
videos: string[];
}
const VideoDownloader: React.FC<VideoDownloaderProps> = ({ videos }) => {
const [progress, setProgress] = useState({} as { [key: string]: string });
const [isDownloading, setIsDownloading] = useState(false);
async function initialSetup(vids: string[]) {
const existingKeys = await keys();
setProgress(
vids.reduce<{ [key: string]: string }>((a, b) => {
a[b] = existingKeys.indexOf(b) > -1 ? "downloaded" : "queued";
return a;
}, {})
);
}
useEffect(() => {
initialSetup(videos);
}, [videos]);
async function download() {
setIsDownloading(true);
const existingKeys = await keys();
for (const videoUrl of videos) {
if (existingKeys.indexOf(videoUrl) === -1) {
setProgress({ ...progress, [videoUrl]: "downloading" });
const response = await fetch(videoUrl);
const videoBlob = await response.blob();
await set(videoUrl, videoBlob);
}
setProgress({ ...progress, [videoUrl]: "downloaded" });
}
setIsDownloading(false);
}
return (
<div>
<button disabled={isDownloading} onClick={download}>
Download Videos
</button>
{Object.keys(progress).map(url => (
<p key={url}>{`${url} - ${progress[url]}`}</p>
))}
</div>
);
};
Essentially, this iterates through a list of URLs, downloads them, and then sets the URL in state to "downloaded". However, the behavior I'm seeing is that the URL shifts from "queued" to "downloading" and then back to "queued" once the next URL begins downloading.
I think the culprit is this line:
setProgress({ ...progress, [videoUrl]: "downloaded" });
I think progress is always in the same state it was when download executes.
Prior to Hooks, I could pass an updater function to setState, but I'm not sure how to reuse existing state in a useState hook.
You can pass an updater function just like with setState. So, in this code, you'd run:
setProgress(progress => ({ ...progress, [videoUrl]: "downloading" }));
This will pass the current value of progress allowing you to update the state based on its current value.

Resources