Event-based refetching

Automatically refetch queries on window focus, network reconnect, or your own custom events

Requires ≥ 4.2

A query's data can grow stale while a user switches to another tab, loses their network connection, or sends your app to the background. Apollo Client can refetch queries automatically when these events happen, so that you serve fresh query data.

This guide walks through enabling event-based refetching, controlling which queries respond to which events, and extending the system with your own custom events.

Refetching on events

Event-based refetching is powered by the RefetchEventManager. It orchestrates refetches when events fire, routing each source to a handler.

Initialize RefetchEventManager and pass it to your ApolloClient instance:

TypeScript
1import { ApolloClient, RefetchEventManager } from "@apollo/client";
2
3const refetchEventManager = new RefetchEventManager();
4
5const client = new ApolloClient({
6  // ...
7  refetchEventManager,
8});

By itself, the refetch event manager doesn't know what events should trigger refetches. For that, it needs event sources that tell it what to listen for and when to trigger refetches.

Sources

A source is a function that returns an Observable that emits each time that event should trigger a refetch. Sources can optionally emit values that become the event's payload, which is used by refetchOn callbacks to conditionally refetch a query. Register sources by name on the sources option:

TypeScript
1const refetchEventManager = new RefetchEventManager({
2  sources: {
3    windowFocus: windowFocusSource,
4  },
5});

When a source emits, the event is routed to its handler which triggers the refetch. The default handler refetches active queries, filtered by their refetchOn option.

Built-in sources

Apollo Client includes built-in sources for common browser events.

Window focus

windowFocusSource triggers a refetch whenever the browser tab is focused. It listens for the visibilitychange event and emits when document.visibilityState is visible.

Assign it to the windowFocus event:

TypeScript
1import { RefetchEventManager, windowFocusSource } from "@apollo/client";
2
3const refetchEventManager = new RefetchEventManager({
4  sources: {
5    windowFocus: windowFocusSource,
6  },
7});
note
This source is safe to use in server environments. It checks whether the window object is available before it registers its event listener.

Network reconnect

onlineSource triggers a refetch when the browser regains network connectivity. It listens for the online event and emits when the event is triggered.

Assign it to the online event:

TypeScript
1import { RefetchEventManager, onlineSource } from "@apollo/client";
2
3const refetchEventManager = new RefetchEventManager({
4  sources: {
5    online: onlineSource,
6  },
7});
note
This source is safe to use in server environments. It checks whether the window object is available before it registers its event listener.

Custom sources

You aren't limited to using the built-in events. You can register your own events that trigger refetches with custom sources.

Let's build a custom source for a React Native app that refetches queries when the app is brought into the foreground. We'll register the source under an event named appState in the sources option.

tip
To avoid TypeScript errors, you must register the event with the RefetchEvents interface. See the TypeScript section to learn more.
TypeScript
1import { AppState } from "react-native";
2import { Observable, ApolloClient, RefetchEventManager } from "@apollo/client";
3
4const appStateSource: RefetchEventManager.EventSource<void> = () => {
5  return new Observable((observer) => {
6    // Attach the event listener when the observable is subscribed to
7    const subscription = AppState.addEventListener("change", (status) => {
8      if (status === "active") {
9        // Emit a `next` event to tell the manager to trigger a refetch
10        observer.next();
11      }
12    });
13
14    return () => {
15      // Cleanup when the observable is unsubscribed
16      subscription.remove();
17    };
18  });
19};
20
21const refetchEventManager = new RefetchEventManager({
22  sources: {
23    appState: appStateSource,
24  },
25});

Source payloads

Sources can optionally emit payloads which are used by refetchOn callbacks to conditionally refetch the query.

tip
To avoid TypeScript errors, register a non-void value type with the event declared in the RefetchEvents interface. See the TypeScript section to learn more.

Let's update the example from the previous section to emit the AppStateStatus with the event and remove the conditional check to allow individual queries to determine whether to refetch.

TypeScript
1import type { AppStateStatus } from "react-native";
2
3const appStateSource: RefetchEventManager.EventSource<AppStateStatus> = () => {
4  return new Observable((observer) => {
5    const subscription = AppState.addEventListener("change", (status) => {
6      if (status === "active") {
7        observer.next(status);
8      }
9    });
10
11    return () => {
12      subscription.remove();
13    };
14  });
15};
caution
You probably don't want this behavior in a real application because this causes refetches for any change event emitted by AppState. If you choose to use this implementation, we recommend pairing it with a default refetchOn provided to defaultOptions.watchQuery.refetchOn that limits refetches to active status.

Disable event refetching for a query

Not all queries need to refetch for every registered event. For example, a query that fetches static configuration data has no reason to refetch when the user focuses the browser tab.

To opt a query out of a single event, set that event in the refetchOn option to false:

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: {
3    windowFocus: false,
4  },
5});

This query disables refetches when the windowFocus event triggers, but continues to refetch for all other registered events, such as online.

Disable refetches for all events

To opt a query out of refetching for all events, set the refetchOn option to false:

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: false,
3});

false is treated the same as if every event is set to false in a refetchOn object.

Conditionally refetch with a callback

More advanced cases might need conditional logic to determine whether to refetch the query at the time an event fires. Pass a callback function for the event in the refetchOn option. It is called with the event payload when that event triggers. Return true to allow the refetch or false to skip it.

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: {
3    // `payload` is the value emitted by the source
4    appState: ({ payload }) => payload === "active",
5  },
6});

Events not passed to the object continue to use the default behavior. In this example, the online event still refetches the query.

note
Your callback function might not be called if the handler first decides to skip refetching for the query.

Generic callbacks

Pass a callback function to the refetchOn option to add conditional logic for any event that would cause a refetch of the query. The function receives a context object with the source name and the event's payload.

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: ({ source, payload }) => {
3    if (source === "appState") {
4      return payload === "active";
5    }
6
7    return true;
8  },
9});

Switch to opt-in by default

The examples so far rely on Apollo Client's default behavior, which refetches every active query when an event fires. To flip the model so queries only refetch when they explicitly opt in, set defaultOptions.watchQuery.refetchOn to false on the client:

TypeScript
1const client = new ApolloClient({
2  refetchEventManager,
3  defaultOptions: {
4    watchQuery: {
5      refetchOn: false,
6    },
7  },
8});

With this configuration, queries must opt-in to the events they are interested in:

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: {
3    windowFocus: true,
4  },
5});

Set refetchOn to true to enable refetches for all events:

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: true,
3});

When both defaultOptions.watchQuery.refetchOn and a per-query refetchOn are objects, Apollo Client merges the two together, with the per-query option taking precedence. This lets you set baseline behavior at the client level and override individual events per query.

Triggering events manually

RefetchEventManager exposes an emit method that imperatively triggers an event. This is useful for testing, for triggering refetches from outside the listener (for example, from a global error handler), or as a way to trigger events that have no automatic source.

emit accepts the name of the event along with its payload, if required.

TypeScript
1refetchEventManager.emit("windowFocus", new Event("visibilitychange"));
note
Calling emit for an event without a registered source or connected client results in a no-op and a development-only warning.

Sourceless events

Some events have no automatic detection logic and exist only to be triggered manually. Register the source and set its value to true. This ensures the event can emit and trigger refetches. If it is not registered, it results in a no-op and a development-only warning is logged.

TypeScript
1const refetchEventManager = new RefetchEventManager({
2  sources: {
3    manualRefresh: true,
4  },
5});
6
7refetchEventManager.emit("manualRefresh");

Like automatic sources, queries can opt-out of refetches by setting the refetchOn option for the event to false:

TypeScript
1const { data } = useQuery(GET_TODOS, {
2  refetchOn: {
3    manualRefresh: false,
4  },
5});
tip
To avoid TypeScript errors, you still need to register sourceless events with TypeScript so that emit calls and refetchOn keys stay type-checked. See the TypeScript section guide for how to type sourceless events and void event payloads.

Customize the refetch handler

When an event fires, RefetchEventManager routes the event to a refetch handler. It is responsible for triggering refetches for the event by calling client.refetchQueries and filtering each query by its refetchOn option. The refetch handler is called whenever a matching source emits an event.

RefetchEventManager implements a default refetch handler for all events that don't provide a custom handler. It refetches all active queries, filtered by each query's refetchOn option.

You can customize the handler for an event by passing a function for an event in the handlers option. The function receives a context object that contains the client instance connected to it, the event source and payload, as well as a matchesRefetchOn helper function that provides the logic necessary to filter queries based on their refetchOn option.

The following example implements a custom refetch handler for the manualRefresh source from the previous section that refetches all queries, including standby queries:

TypeScript
1const manualRefreshHandler: RefetchEventManager.EventHandler<
2  "manualRefresh"
3> = ({ client, matchesRefetchOn }) => {
4  return client.refetchQueries({
5    include: "all",
6    onQueryUpdated: (observableQuery) => {
7      return matchesRefetchOn(observableQuery);
8    },
9  });
10};
11
12export const refetchEventManager = new RefetchEventManager({
13  sources: {
14    manualRefresh: true,
15  },
16  handlers: {
17    manualRefresh: manualRefreshHandler,
18  },
19});

Using matchesRefetchOn

The handler context includes a matchesRefetchOn helper function that evaluates a query's refetchOn option. Use it inside the onQueryUpdated callback function to filter out the query when it opts out of refetches for the event.

TypeScript
1const manualRefreshHandler: RefetchEventManager.EventHandler<
2  "manualRefresh"
3> = ({ client, matchesRefetchOn }) => {
4  return client.refetchQueries({
5    include: "all",
6    onQueryUpdated: (observableQuery) => {
7      return matchesRefetchOn(observableQuery);
8    },
9  });
10};
caution
You must use matchesRefetchOn to correctly filter the query. If you don't use this function, included queries always refetch regardless of whether they opt out of refetching.

You can compose matchesRefetchOn with additional logic when a handler needs to evaluate the refetchOn option with it.

TypeScript
1const manualRefreshHandler: RefetchEventManager.EventHandler<
2  "manualRefresh"
3> = ({ client, matchesRefetchOn }) => {
4  return client.refetchQueries({
5    include: "all",
6    onQueryUpdated: (observableQuery) => {
7      return (
8        matchesRefetchOn(observableQuery) &&
9        observableQuery.options.fetchPolicy !== "no-cache"
10      );
11    },
12  });
13};

Skip a refetch from a handler

You can conditionally skip entire refetches by returning void from a handler.

TypeScript
1const manualRefreshHandler: RefetchEventManager.EventHandler<
2  "manualRefresh"
3> = ({ client, matchesRefetchOn }) => {
4  if (document.visibilityState === "visible") {
5    return client.refetchQueries({
6      include: "all",
7      onQueryUpdated: (observableQuery) => {
8        return (
9          matchesRefetchOn(observableQuery) &&
10          observableQuery.options.fetchPolicy !== "no-cache"
11        );
12      },
13    });
14  }
15};
tip
Use this sparingly and prefer using refetchOn instead to ensure more predictable behavior.

TypeScript

Register custom events with TypeScript to provide full type safety for refetch events. Declare your custom events using TypeScript's declaration merging with the RefetchEvents interface.

Each property on RefetchEvents maps an event name to its payload type.

TypeScript
apollo.d.ts
1import "@apollo/client";
2import type { AppStateStatus } from "react-native";
3
4declare module "@apollo/client" {
5  interface RefetchEvents {
6    appState: AppStateStatus;
7  }
8}

With this declaration in place, TypeScript provides type safety for source functions, refetch handler functions, the emit method, and the refetchOn option.

Optional payloads

To make an event's payload optional, include undefined in the payload type. This makes the payload optional when the source observable emits, or when you imperatively trigger the event with emit.

TypeScript
apollo.d.ts
1import "@apollo/client";
2
3declare module "@apollo/client" {
4  interface RefetchEvents {
5    manualRefresh: { reason: string } | undefined;
6  }
7}
TypeScript
1// ✅ valid
2refetchEventManager.emit("manualRefresh");
3
4// ✅ valid
5refetchEventManager.emit("manualRefresh", { reason: "manual refresh" });

Events without payloads

If an event doesn't have an associated payload, set its payload type to void. This makes it so that you don't pass a payload in the source observable or when you imperatively trigger the event with emit.

TypeScript
apollo.d.ts
1import "@apollo/client";
2
3declare module "@apollo/client" {
4  interface RefetchEvents {
5    manualRefresh: void;
6  }
7}
TypeScript
1// ✅ valid
2refetchEventManager.emit("manualRefresh");
3
4// ❌ invalid
5refetchEventManager.emit("manualRefresh", anyValue);

See also

See the refetching guide to learn more about the client.refetchQueries API.