Guides
Window focus refetching
Stale queries on screen refetch when the user comes back to the tab or reconnects. How to turn it off, globally or per read.
When the user leaves your app and comes back, the data on screen may be out of date. raf-query refetches every read, stale query when the page becomes visible again, and when the browser reconnects after being offline.
Only queries that are being read refetch, and only stale ones: data within its staleTime stays as it is. With the default staleTime of 0, that's everything on screen.
Turning it off
For every query, in the client's defaults:
const const queryClient: QueryClient<never>queryClient = new new QueryClient<never>(config?: QueryClientConfig<never>): QueryClient<never>QueryClient({
Config.defaults?: QueryBehavior | undefineddefaults: {
QueryBehavior.refetchOnReconnect?: boolean | undefinedrefetchOnReconnect: false,
QueryBehavior.refetchOnWindowFocus?: boolean | undefinedrefetchOnWindowFocus: false,
},
});For one query, in its factory:
export function function settingsQuery(): QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>
settingsQuery() {
return queryOptions<{
readonly theme: string;
}, unknown, never, readonly ["settings"], undefined>(options: Omit<QueryOptions<{
data: {
readonly theme: string;
};
error: unknown;
key: readonly ["settings"];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>
queryOptions({
queryKey: readonly ["settings"]queryKey: ["settings"],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: {
readonly theme: string;
};
error: unknown;
key: readonly ["settings"];
services: never;
}>
queryFn: () => function getSettings(): Promise<{
readonly theme: string;
}>
getSettings(),
refetchOnWindowFocus?: boolean | undefinedrefetchOnWindowFocus: false,
});
}
Or for one reader, in useQuery's options. The query refetches on focus if any reader wants it to:
const [const settings: Thenable<{
readonly theme: string;
}>
settings] = useQuery<() => QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>, undefined>(factory: () => QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>, handle: QueryHandle<() => QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [promise: Thenable<{
readonly theme: string;
}>, controls: QueryControls<() => QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>>] (+1 overload)
useQuery(function settingsQuery(): QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>
settingsQuery, const loaderData: {
settings: QueryHandle<typeof settingsQuery>;
}
loaderData.settings: QueryHandle<() => QueryOptions<Flat<{
data: {
readonly theme: string;
};
key: readonly ["settings"];
}>>>
settings, {
Options.refetchOnWindowFocus?: boolean | undefinedrefetchOnWindowFocus: false,
});A long staleTime is often the better fix: the data then refetches on focus only once it's actually old.
What counts as focus
The page counts as focused when document.visibilityState becomes "visible": switching back to the tab, restoring the window, unlocking the phone. Clicking between two visible windows isn't a visibility change, so it doesn't refetch.
To test it by hand, switch tabs and come back. The polling demo shows it too: polling pauses while the tab is hidden.
A custom platform
The cache learns about focus and network changes from a Platform service. Its default reads window, document and navigator. Provide your own for environments without them (React Native, an embedded webview) or to control focus in tests:
const const listeners: Set<(event: "focus" | "online") => void>listeners = new var Set: SetConstructor
new <(event: "focus" | "online") => void>(iterable?: Iterable<(event: "focus" | "online") => void> | null | undefined) => Set<(event: "focus" | "online") => void> (+1 overload)
Set<(event: "focus" | "online"event: "focus" | "online") => void>();
const const AppPlatform: Layer.Layer<never, never, never>AppPlatform = import LayerLayer.const succeed: <never, Platform.Platform>(service: Key<never, Platform.Platform>, resource: Platform.Platform) => Layer.Layer<never, never, never> (+1 overload)succeed(import PlatformPlatform.const Platform: Reference<Platform.Platform>Platform, {
Platform.isOnlineUnsafe(): booleanisOnlineUnsafe: () => true,
Platform.isServer: booleanisServer: false,
Platform.isVisibleUnsafe(): booleanisVisibleUnsafe: () => const AppState: {
readonly currentState: string;
addEventListener(event: "change", listener: (state: string) => void): void;
}
AppState.currentState: stringcurrentState === "active",
Platform.subscribeUnsafe(listener: (event: Platform.PlatformEvent) => void): () => voidsubscribeUnsafe(listener: (event: Platform.PlatformEvent) => voidlistener) {
const listeners: Set<(event: "focus" | "online") => void>listeners.Set<(event: "focus" | "online") => void>.add(value: (event: "focus" | "online") => void): Set<(event: "focus" | "online") => void>add(listener: (event: Platform.PlatformEvent) => voidlistener);
return () => {
const listeners: Set<(event: "focus" | "online") => void>listeners.Set<(event: "focus" | "online") => void>.delete(value: (event: "focus" | "online") => void): booleandelete(listener: (event: Platform.PlatformEvent) => voidlistener);
};
},
});
const AppState: {
readonly currentState: string;
addEventListener(event: "change", listener: (state: string) => void): void;
}
AppState.function addEventListener(event: "change", listener: (state: string) => void): voidaddEventListener("change", (state: stringstate) => {
if (state: stringstate === "active") {
for (const const listener: (event: "focus" | "online") => voidlistener of const listeners: Set<(event: "focus" | "online") => void>listeners) const listener: (event: "focus" | "online") => voidlistener("focus");
}
});
const const queryClient: QueryClient<never>queryClient = new new QueryClient<never>(config?: QueryClientConfig<never>): QueryClient<never>QueryClient({ QueryClientConfig<never>.layer?: Layer.Layer<never, never, never> | undefinedlayer: const AppPlatform: Layer.Layer<never, never, never>AppPlatform });The cache is built on top of the client's layer, so the platform it provides replaces the default.
See Testing for using it in tests.