Skip to content

raf-query is early. The API can still change between minor releases, so use it at your own risk.

raf-query

Guides

Polling

Refetch a query on an interval while it's on screen with meta.refetchInterval and the polling() feature.

A query with meta: { refetchInterval } refetches every so many milliseconds while a component reads it:

export function 
function buildQuery(input: {
    id: string;
}): QueryOptions<readonly ["builds", string], Build, Build, DefaultTypes>
buildQuery
(
input: {
    id: string;
}
input
: { id: stringid: string }) {
return
queryOptions<readonly ["builds", string], Build, Build>(options: BaseOptions<readonly ["builds", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["builds", string]>) => Awaitable<Build>;
    readonly select?: ((data: Build) => Build) | undefined;
}): QueryOptions<readonly ["builds", string], Build, Build, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["builds", string], DefaultTypes>.queryKey: readonly ["builds", string]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
: builds<readonly [string]>(rest_0: string): readonly ["builds", string]builds(
input: {
    id: string;
}
input
.id: stringid),
queryFn: (context: QueryFunctionContext<readonly ["builds", string]>) => Awaitable<Build>queryFn: () => function getBuild(id: string): Promise<Build>getBuild(
input: {
    id: string;
}
input
.id: stringid),
BaseOptions<readonly ["builds", string], DefaultTypes>.meta?: NoInfer<Partial<Readonly<Record<string, unknown>>>> | undefined

Settings for the root's links and features, and data for targets: the fields the root's parts declare, such as retry with the retry() link.

meta
: { refetchInterval: numberrefetchInterval: 2000 },
}); }

The polling() feature reads that field, so the client needs it, on its own or through recommended():

const 
const queryClient: QueryClient<{
    readonly meta: Flat<Present<PollingMeta>>;
}>
queryClient
=
createQueryClient<[], readonly [Feature<{
    readonly meta: PollingMeta;
}>]>(config?: QueryClientConfig & {
    readonly features?: readonly [Feature<{
        readonly meta: PollingMeta;
    }>] | undefined;
    readonly links?: [] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<PollingMeta>>;
}>

Create a query client. Behaviors are opt-in: pass links and features, or spread recommended(). The client's meta type is what they declare.

createQueryClient
({
features?: (readonly AnyFeature[] & readonly [Feature<{
    readonly meta: PollingMeta;
}>]) | undefined

Behaviors started while a provider has the client mounted.

features
: [
function polling(): Feature<{
    readonly meta: PollingMeta;
}>

Refetch read queries every meta.refetchInterval ms.

polling
()] });
// or const
const sameWithMore: QueryClient<{
    readonly meta: Flat<Present<PollingMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<RetryMeta>>;
}>
sameWithMore
=
createQueryClient<readonly [Link<{
    readonly meta: RetryMeta;
}>], readonly [Feature<{
    readonly meta: FocusRefetchMeta;
}>, Feature<{
    readonly meta: ReconnectRefetchMeta;
}>, Feature<{
    readonly meta: PollingMeta;
}>]>(config?: QueryClientConfig & {
    readonly features?: readonly [Feature<{
        readonly meta: FocusRefetchMeta;
    }>, Feature<{
        readonly meta: ReconnectRefetchMeta;
    }>, Feature<...>] | undefined;
    readonly links?: readonly [...] | undefined;
}): QueryClient<...>

Create a query client. Behaviors are opt-in: pass links and features, or spread recommended(). The client's meta type is what they declare.

createQueryClient
({ ...function recommended(): Recommended

The behaviors most apps want: retries, refetching on focus and reconnect, and polling. Spread it into the config:

recommended
() });

On a client without polling(), refetchInterval does nothing. A factory built with a bound queryOptions gets the field typed only when the root has the feature: see Query meta.

  • Polling starts when the first component reads the query, and stops when the last one unmounts.
  • There is one timer per query, however many components read it.
  • A poll that lands while another request for the query is in flight joins it instead of starting a second one.
  • Features run while a QueryClientProvider has the client mounted, so the server never polls.

Each poll is a background refetch: the data on screen stays put. Structural sharing keeps a response equal to the current data from changing the data's identity, so memoized children don't re-render.

The interval per input

The interval is part of the query's options, so a factory can compute it from its input:

export function 
function buildLogQuery(input: {
    id: string;
    live: boolean;
}): QueryOptions<readonly ["builds", "log", {
    id: string;
    live: boolean;
}], Build, Build, DefaultTypes>
buildLogQuery
(
input: {
    id: string;
    live: boolean;
}
input
: { id: stringid: string; live: booleanlive: boolean }) {
return
queryOptions<readonly ["builds", "log", {
    id: string;
    live: boolean;
}], Build, Build>(options: BaseOptions<readonly ["builds", "log", {
    id: string;
    live: boolean;
}], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["builds", "log", {
        id: string;
        live: boolean;
    }]>) => Awaitable<Build>;
    readonly select?: ((data: Build) => Build) | undefined;
}): QueryOptions<readonly ["builds", "log", {
    id: string;
    live: boolean;
}], Build, Build, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["builds", "log", { id: string; live: boolean; }], DefaultTypes>.queryKey: readonly ["builds", "log", {
    id: string;
    live: boolean;
}]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
builds<readonly ["log", {
    id: string;
    live: boolean;
}]>(rest_0: "log", rest_1: {
    id: string;
    live: boolean;
}): readonly ["builds", "log", {
    id: string;
    live: boolean;
}]
builds
("log",
input: {
    id: string;
    live: boolean;
}
input
),
queryFn: (context: QueryFunctionContext<readonly ["builds", "log", {
    id: string;
    live: boolean;
}]>) => Awaitable<Build>
queryFn
: () => function getBuild(id: string): Promise<Build>getBuild(
input: {
    id: string;
    live: boolean;
}
input
.id: stringid),
BaseOptions<readonly ["builds", "log", { id: string; live: boolean; }], DefaultTypes>.meta?: NoInfer<Partial<Readonly<Record<string, unknown>>>> | undefined

Settings for the root's links and features, and data for targets: the fields the root's parts declare, such as retry with the retry() link.

meta
: { refetchInterval: number | booleanrefetchInterval:
input: {
    id: string;
    live: boolean;
}
input
.live: booleanlive ? 1000 : false },
}); }

false or 0 turns polling off. Each input is its own query, so this picks the interval per query; it can't change while the same query is read.

Polling and staleTime

Polling refetches whatever the data's staleTime: the interval is an explicit request for fresh data. Keep staleTime meaningful anyway, because the loaders that start the same query rely on it.