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

Reading queries

useQuery returns a stable thenable and the query's state; read it with use() under Suspense, switch on its status, or pass it down to a child.

Components read queries with useQuery, passing the factory and the handle the loader returned:

const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
,
const postState: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, QueryState<...>]

Read a query by its handle.

useQuery
(
const factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
factory
,
const handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
, const options: UseQueryOptions | undefinedoptions);
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
);

postThenable is a stable Thenable<Data> owned by the cache: a Promise that also carries status, value and reason, the fields React's use() reads. Read it with use() on its own line. When the cache already has the data, the thenable is fulfilled and use() returns it synchronously; otherwise use() suspends until it lands.

useQuery returns a thenable for every handle. A handle from swr or revalidate had its data loaded before the route rendered, so use() doesn't suspend on it. If the cache dropped the data since (garbage collection, resetOne, removeOne), use() suspends while the query refetches. See Handles.

function function PostPage(): React.JSX.ElementPostPage() {
	const 
const loaderData: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
loaderData
=
const Route: Route<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 15 more ..., undefined>
Route
.
RouteExtensions<"/posts/$slug", "/posts/$slug">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/posts/$slug", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
// `post` came from `swr`, `comments` from `prefetch`. const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
,
const postState: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
,
const loaderData: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
loaderData
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
);
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
);
return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={
const postState: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState
.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.isFetching: booleanisFetching}
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => function startTransition(scope: React.TransitionFunction): void

Similar to useTransition but allows uses where hooks are not available.

@paramcallback — A function which causes state updates that can be deferred.
startTransition
(() =>
const postState: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState
.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.refetch(): Promise<void>

Fetch again, or join the request in flight. Resolves when it settles.

refetch
())}
> Refresh </React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const post: Postpost.Post.body: stringbody}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> <const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
React.SuspenseProps.fallback?: React.ReactNode

A fallback react tree to show when a Suspense child (like React.lazy) suspends

fallback
={<function CommentsSkeleton(): React.ReactNodeCommentsSkeleton />}>
<
function Comments(props: {
    handle: QueryHandle<typeof commentsQuery>;
}): React.ReactNode
Comments
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const loaderData: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
loaderData
.
comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); }

options takes whilePending. There is no option to change the input: the handle decides what is read. Settings such as polling belong to the query, as meta in its factory.

State

FieldMeaning
isPendingNo data yet for the handle's input; use() on the thenable will suspend
isPreviousThe thenable holds the previous handle's data while this handle's query loads (whilePending: "previous")
isFetchingA request is in flight: first load, new input, or background refetch
isStaleData is older than staleTime, or was invalidated
errorThe last request's error while it's the latest outcome
updatedAtWhen the handle's data was fetched; 0 without data
queryKey, inputThe handle's key and input
hashStable string form of the handle's key, for key props on Suspense boundaries
refetch()Refetch now. Returns a promise, so it can be awaited inside a transition or action

Paged factories read with useInfiniteQuery, whose state adds fetchNextPage(), fetchPreviousPage(), hasNextPage, hasPreviousPage, isFetchingNextPage and isFetchingPreviousPage. See Infinite queries.

Updates never suspend

When the cache updates — refetch, invalidation, setData — the hook returns a new, fulfilled thenable and the component re-renders without suspending. New data is structurally shared with the old: a refetch that returns equal data keeps the same thenable and the same objects, so memoized children don't re-render. See Render optimizations.

Errors

When the first load fails, the thenable rejects and use() throws to the nearest error boundary. A failed background refetch doesn't throw: the old data stays on screen and the state's error holds the error, so the component can show it next to the data. Retries has a demo of both.

Before resetting the error boundary, call resetOne(qc, factory, input), so the query loads again instead of throwing the same error.

Passing thenables down

A component can call the hook with a prefetched handle and hand the thenable to a child, so only the child suspends:

const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
,
const loaderData: {
    post: QueryHandle<typeof postQuery>;
}
loaderData
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
); // from `prefetch`
return ( <const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
React.SuspenseProps.fallback?: ReactNode

A fallback react tree to show when a Suspense child (like React.lazy) suspends

fallback
={<function Spinner(): ReactNodeSpinner />}>
<
function PostBody(props: {
    post: Promise<Post>;
}): React.JSX.Element
PostBody
post: Promise<Post>post={
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
>
);

The component that calls useQuery re-renders when its thenable changes, and when a state field it read changes, such as isFetching during a background refetch. A parent that should stay put can pass the handle down instead, so only the child reads the query. See Tracked state.

Because use() can be called conditionally, a component can also decide whether to read:

function 
function Details(props: {
    open: boolean;
    details: Promise<Details>;
}): React.JSX.Element | null
Details
(
props: {
    open: boolean;
    details: Promise<Details>;
}
props
: { open: booleanopen: boolean; details: Promise<Details>details: interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<Details> }) {
if (!
props: {
    open: boolean;
    details: Promise<Details>;
}
props
.open: booleanopen) return null;
const const details: Detailsdetails = use<Details>(usable: React.Usable<Details>): Detailsuse(
props: {
    open: boolean;
    details: Promise<Details>;
}
props
.details: Promise<Details>details);
return <
function DetailsTable(props: {
    details: Details;
}): React.ReactNode
DetailsTable
details: Detailsdetails={const details: Detailsdetails} />;
}

No refetch on mount

useQuery never refetches stale data on mount: whatever started the query decides. Stale data refetches when a loader or server component starts the query again, when it's invalidated (a reader that mounts after an invalidation refetches it), on refetch(), and with the client's features: window focus and reconnect refetching, and polling. A reader only fetches on its own when the query has no data, which is a waterfall.

That means rendering the same handle in many components is free: they share one entry and one request. See Background fetching indicators to show when a refetch is running.