Guides
Render optimizations
Structural sharing, stable promises and tracked controls keep re-renders to the components whose data actually changed.
raf-query re-renders a component only when something it uses has changed. Three mechanisms make that happen, all on by default.
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 promise doesn't change: nothing re-renders at all.
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:
queryOptions<unknown, unknown, never, readonly ["geojson", string], undefined>(options: Omit<QueryOptions<{
data: unknown;
error: unknown;
key: readonly ["geojson", string];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: unknown;
key: readonly ["geojson", string];
}>>
queryOptions({
queryKey: readonly ["geojson", string]queryKey: ["geojson", const input: {
region: string;
}
input.region: stringregion],
function queryFn(context: QueryFunctionContext): unknownqueryFn: () => function getRegionShapes(input: {
readonly region: string;
}): Promise<unknown>
getRegionShapes(const input: {
region: string;
}
input),
structuralSharing?: boolean | undefinedstructuralSharing: false,
});Stable promises
useQuery returns a promise 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 promise is always pre-resolved: use() returns its value synchronously, and nothing suspends on a refetch.
Tracked controls
The controls object returned by useQuery tracks which fields a component reads. Reading controls.isFetching subscribes the component to isFetching; a component that never reads it isn't re-rendered when a background refetch starts and ends.
function function PostPage(): React.JSX.ElementPostPage() {
const const loaderData: {
comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>;
post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>;
}
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();
// Reads only the promise: doesn't re-render while refetching.
const [const postPromise: PostpostPromise] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [data: Post, controls: QueryControls<...>] (+1 overload)
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, const loaderData: {
comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>;
post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>;
}
loaderData.post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post);
return (
<>
<function RefreshButton(props: {
handle: QueryHandleWithData<typeof postQuery>;
}): React.JSX.Element
RefreshButton handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handle={const loaderData: {
comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>;
post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>;
}
loaderData.post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: ReactNodefallback={<function PostSkeleton(): ReactNodePostSkeleton />}>
<function PostBody(props: {
readonly post: unknown;
}): ReactNode
PostBody post: unknownpost={const postPromise: PostpostPromise} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</>
);
}
function function RefreshButton(props: {
handle: QueryHandleWithData<typeof postQuery>;
}): React.JSX.Element
RefreshButton(props: {
handle: QueryHandleWithData<typeof postQuery>;
}
props: {
handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handle: interface QueryHandleWithData<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandleWithData<typeof function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery>;
}) {
// Reads isFetching: re-renders when a request starts and ends.
const [, const post: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [data: Post, controls: QueryControls<...>] (+1 overload)
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, props: {
handle: QueryHandleWithData<typeof postQuery>;
}
props.handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handle);
return (
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={const post: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post.QueryControls<(input: { slug: string; }) => QueryOptions<Flat<{ data: Post; key: readonly ["post", string]; }>>>.isFetching: booleanisFetching} React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => const post: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post.QueryControls<(input: { slug: string; }) => QueryOptions<Flat<{ data: Post; key: readonly ["post", string]; }>>>.refetch(): Promise<void>refetch()}>
Refresh
</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
);
}
Both components read the same query; each pays only for what it reads. Destructure only what you use. Spreading the controls ({ ...controls }) reads every field and subscribes to all of them.
Passing promises down
A parent can read a prefetched handle and hand the promise to a child, so only the child suspends. The parent re-renders when the promise changes, which is exactly when the child needs new props:
const [const postPromise: Thenable<Post>postPromise] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [promise: Thenable<Post>, controls: QueryControls<...>] (+1 overload)
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, const loaderData: {
post: QueryHandle<typeof postQuery>;
}
loaderData.post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post);
return (
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: ReactNodefallback={<function Spinner(): ReactNodeSpinner />}>
<function PostBody(props: {
readonly post: Promise<unknown>;
}): ReactNode
PostBody post: Promise<unknown>post={const postPromise: Thenable<Post>postPromise} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
);
Deriving data
There's no select option. Derive in render: the data only changes when it actually changed, so with React Compiler (or useMemo) the derivation reruns only then:
const const todos: readonly Todo[]todos = use<readonly Todo[]>(usable: React.Usable<readonly Todo[]>): readonly Todo[]use(const todosPromise: Promise<readonly Todo[]>todosPromise);
const const done: Todo[]done = const todos: readonly Todo[]todos.ReadonlyArray<Todo>.filter(predicate: (value: Todo, index: number, array: readonly Todo[]) => unknown, thisArg?: any): Todo[] (+1 overload)filter((todo: Todotodo) => todo: Todotodo.Todo.done: booleandone);To share a derivation, put it in a function next to the factory, and call it wherever the data is read.