T
TanStack14mo ago
conscious-sapphire

App-level polling with react-query

Hello, I am dealing with asynchronous API. In other words, I have mutation (POST/PUT) requests that will trigger async jobs in the back-end. These job can take 10-20s to complete, and in order to keep my front-end synchronized I am doing conditional polling.
const { isSuccess } = useMyMutation({})

const myQuery = useGetMyData({
refetchInterval: query => {
const isFresh = isSuccess && isDataFresh(query.state.data)
return isFresh ? false : 5000
}
})
const { isSuccess } = useMyMutation({})

const myQuery = useGetMyData({
refetchInterval: query => {
const isFresh = isSuccess && isDataFresh(query.state.data)
return isFresh ? false : 5000
}
})
This code is fine, but the polling is done only at component-level, because I need to check that the mutation is successful and rely on some data comparison to determine if the polling should be processed. Unfortunately, whenever I unmount my component, the polling will stop and other components that are observer of the useGetMyData will not poll. Idea: I was thinking of implementing in-house app-level polling like:
useMyMutation({
onSuccess: () => {
appPolling.add({
interval: 5000,
process: () => {
invalidateUseMyDataQuery()
},
stopOn: <MyDataType>(data) => {
isDataFresh(query.state.data)
}
})
}
})
useMyMutation({
onSuccess: () => {
appPolling.add({
interval: 5000,
process: () => {
invalidateUseMyDataQuery()
},
stopOn: <MyDataType>(data) => {
isDataFresh(query.state.data)
}
})
}
})
The code seems a bit magic, probably it will be different to properly handle callback functions and typesafety. This solution is still not perfect, because the user can send the mutation and navigate to another route before the onSuccess is triggered. Do you think there are better/easier way to do app-level polling ? Thanks 🙂
5 Replies
conscious-sapphire
conscious-sapphireOP14mo ago
Other solution would be to have global app state, so I can do polling in a top-level component 🤷‍♂️
flat-fuchsia
flat-fuchsia14mo ago
Move this logic to the App or any other top-level component that doesn't unmount. If you need to access mutation data you can use https://tanstack.com/query/latest/docs/framework/react/reference/useMutationState
useMutationState | TanStack Query React Docs
useMutationState is a hook that gives you access to all mutations in the MutationCache. You can pass filters to it to narrow down your mutations, and select to transform the mutation state. Example 1: Get all variables of all running mutations
conscious-sapphire
conscious-sapphireOP14mo ago
@denis.monastyrskyi If I understand correctly I need to: - Keep the useMutation as it is - Add a useMutationState at the top-level to handle onSuccess - Add queries that depends on onSuccess at the top-level to handle polling
flat-fuchsia
flat-fuchsia14mo ago
If this logic is highly related I would create a custom hook for it and register it somewhere at the App level. Like so
export function usePolling() {
const mutationState = useMutationState({
filters: { mutationKey: ['someKey'] },
});

useGetMyData({
refetchInterval: query => {
const isFresh = mutationState.isSuccess && isDataFresh(query.state.data)
return isFresh ? false : 5000
}
})
}
export function usePolling() {
const mutationState = useMutationState({
filters: { mutationKey: ['someKey'] },
});

useGetMyData({
refetchInterval: query => {
const isFresh = mutationState.isSuccess && isDataFresh(query.state.data)
return isFresh ? false : 5000
}
})
}
It's just a pseudo code, but You got the point. Since this hook doesn't return anything, it shouldn't cause the App to rerender. You can call it inside the app to start polling. Since the App lives while the browser tab is open, it will poll until your condition is met. Then you can call useMutation across the app as usual but don't forget to provide mutationKey: ['someKey'] to it if you want your usePolling to watch it.
deep-jade
deep-jade14mo ago
Since this hook doesn't return anything, it shouldn't cause the App to rerender
The hook is good, but that's wrong. With useGetMyData, there is a subscription to a query, so it will re-render. But you can put it in an isolated component somewhere in the top of the App so that it doesn't re-render everything

Did you find this page helpful?