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

Render optimizations

Tracked state, structural sharing, stable thenables and select keep readers from re-rendering, and the data's identity stable, when nothing they use changed.

A component that calls useQuery or useInfiniteQuery re-renders only when something it uses changed: its thenable, or a state field it read. The data keeps its identity until its content changes, so memo components, React Compiler and effects below the reader skip work on refetches that changed nothing.

Tracked state

The hooks track which state fields a component reads while it renders. A write re-renders the component only when the thenable changed, the thenable settled, or a field it read changed:

  • A component that reads only the thenable doesn't re-render while a background refetch starts and ends, or when isStale or updatedAt change.
  • One that reads isFetching re-renders when a request starts and when it ends.
  • The state object keeps its identity until a field the component read changes. Its functions (refetch(), fetchNextPage(), fetchPreviousPage()) keep theirs for as long as the component is mounted, so they're safe as effect dependencies and memo props.
  • A field read for the first time in a later render is current, and tracked from then on.

Read fields while rendering. A field read only in an effect or an event handler isn't tracked: it holds its value from the last render, which may be older than the cache.

Structural sharing

When a query refetches, the new data is compared with the old. Objects and arrays that are deeply equal keep their old identity:

// Before: { id: 1, author: { name: "Ada" }, tags: ["a"] }
// After:  { id: 1, author: { name: "Ada" }, tags: ["a", "b"] }
// `after.author === before.author`: unchanged parts keep their identity.

This lets memo components and React Compiler skip re-rendering children whose props didn't change, and keeps effects from re-running on refetches that changed nothing.

When the whole response is deeply equal to the cached data, the old data is kept as it is, and the reader's thenable doesn't change.

Structural sharing only looks inside plain objects and arrays. Class instances, Maps and Dates are compared by identity. Turn it off for very large responses where the comparison costs more than it saves, per query or for the whole client with createQueryClient({ structuralSharing: false }):

queryOptions<readonly ["maps", "shapes", string], unknown, unknown>(options: BaseOptions<readonly ["maps", "shapes", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["maps", "shapes", string]>) => unknown;
    readonly select?: ((data: unknown) => unknown) | undefined;
}): QueryOptions<readonly ["maps", "shapes", string], unknown, unknown, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
:
const maps: Group
<readonly ["shapes", string]>(rest_0: "shapes", rest_1: string) => readonly ["maps", "shapes", string]
maps
("shapes",
const input: {
    region: string;
}
input
.region: stringregion),
queryFn: (context: QueryFunctionContext<readonly ["maps", "shapes", string]>) => unknownqueryFn: () =>
function getRegionShapes(input: {
    readonly region: string;
}): Promise<unknown>
getRegionShapes
(
const input: {
    region: string;
}
input
),
BaseOptions<TKey extends QueryKey, TTypes extends RootTypes = RootTypes>.structuralSharing?: boolean | undefined

Keep the deeply equal parts of new data, so they keep their identity. Default true.

structuralSharing
: false,
});

Stable thenables

useQuery returns a thenable owned by the cache. It changes only when the data does, so it's safe to pass down, compare, or use as a dependency. A new thenable is always already fulfilled: use() returns its value synchronously, and nothing suspends on a refetch.

Selecting data

select in a factory derives what readers get from the queryFn's data:

export function 
function postTitleQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "title", string], Post, string, DefaultTypes>
postTitleQuery
(
input: {
    slug: string;
}
input
: { slug: stringslug: string }) {
return
queryOptions<readonly ["posts", "title", string], Post, string>(options: BaseOptions<readonly ["posts", "title", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "title", string]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => string) | undefined;
}): QueryOptions<readonly ["posts", "title", string], Post, string, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
: posts<readonly ["title", string]>(rest_0: "title", rest_1: string): readonly ["posts", "title", string]posts("title",
input: {
    slug: string;
}
input
.slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", "title", string]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(
input: {
    slug: string;
}
input
.slug: stringslug),
select?: ((data: Post) => string) | undefinedselect: (post: Postpost) => post: Postpost.Post.title: stringtitle, }); }
  • select runs again only when the cached data changes, so its result keeps its identity across refetches that changed nothing.
  • It's part of the factory, so every reader of the factory gets the selected data. getData and setData work on the data before select.
  • When select's result is unchanged, the reader's thenable is the same, so a component that reads only the thenable doesn't re-render.

For a derivation only one component needs, derive in render. The data only changes when it actually changed, so with React Compiler (or useMemo) the derivation reruns only then:

const const comments: readonly PostComment[]comments = use<readonly PostComment[]>(usable: React.Usable<readonly PostComment[]>): readonly PostComment[]use(const commentsPromise: Promise<readonly PostComment[]>commentsPromise);
const const byAda: PostComment[]byAda = const comments: readonly PostComment[]comments.ReadonlyArray<PostComment>.filter(predicate: (value: PostComment, index: number, array: readonly PostComment[]) => unknown, thisArg?: any): PostComment[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

@parampredicate — A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
filter
((comment: PostCommentcomment) => comment: PostCommentcomment.PostComment.author: stringauthor === "Ada");

Reading where the data is used

Pass the handle down instead of the data, and call useQuery in the components that use it. Each reader re-renders only for what it reads:

function function PostPage(): React.JSX.ElementPostPage() {
	// Passes the handle down; doesn't read the query.
	return (
		<>
			<
function RefreshButton(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
RefreshButton
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
={
const loaderData: {
    post: QueryHandle<typeof postQuery>;
}
loaderData
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
} />
<
function PostBody(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
PostBody
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
={
const loaderData: {
    post: QueryHandle<typeof postQuery>;
}
loaderData
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
} />
<function CommentsSection(): ReactNodeCommentsSection /> </> ); } function
function RefreshButton(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
RefreshButton
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>

A small serializable reference to a query: its key and its factory's input. It carries no data; readers get that from the cache. Handles of different factories have different types.

QueryHandle
<typeof
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
> }) {
const [,
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
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
);
return ( <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={() =>
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> ); } function
function PostBody(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
PostBody
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>

A small serializable reference to a query: its key and its factory's input. It carries no data; readers get that from the cache. Handles of different factories have different types.

QueryHandle
<typeof
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
> }) {
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
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
);
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>{const post: Postpost.Post.body: stringbody}</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>; }

Both components read the same query. When a request starts and ends, RefreshButton re-renders because it reads isFetching; PostBody reads only the thenable and re-renders when the data changes; the page around them doesn't. Handles are small serializable objects that don't change while the page shows the same query, so passing them down costs nothing.

Passing thenables down

A parent can read a prefetched handle and hand the thenable to a child, so only the child suspends. The thenable changes only when the data does, which is exactly when the child needs new props:

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
);
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: {
    readonly post: Promise<unknown>;
}): ReactNode
PostBody
post: Promise<unknown>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
>
);