Skip to content

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

raf-query

Integrations

Effect services

Effect queryFns can require services. They become part of the factory's type, and QueryClient provides them from a layer.

A queryFn's Effect can require services. They're part of the factory's type, ServicesOf<TFactory>, and every verb that can start the query requires them:

class class IssueApiIssueApi extends import ContextContext.
const Service: <IssueApi, {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}>() => <Identifier, E, R, Args>(id: Identifier, options?: {
    readonly make?: ((...args: Args) => Effect.Effect<{
        readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
    }, E, R>) | Effect.Effect<{
        readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
    }, E, R> | undefined;
} | undefined) => Context.ServiceClass<IssueApi, Identifier, {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}> & ([...] extends [...] ? unknown : {
    ...;
}) (+2 overloads)
Service
<
class IssueApiIssueApi, { readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>get: (id: numberid: number) => import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<Issue, class IssueNotFoundIssueNotFound> } >()("app/IssueApi") {} const const IssueApiLive: Layer.Layer<IssueApi, never, never>IssueApiLive = import LayerLayer.
const succeed: <IssueApi, {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}>(service: Context.Key<IssueApi, {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}>, resource: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}) => Layer.Layer<IssueApi, never, never> (+1 overload)
succeed
(class IssueApiIssueApi, { get: (id: number) => Effect.Effect<Issue, IssueNotFound>get:
const IssueRepo: {
    get(id: number): Effect.Effect<Issue, IssueNotFound>;
}
IssueRepo
.function get(id: number): Effect.Effect<Issue, IssueNotFound>get });
export function
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>
issueQuery
(
input: {
    id: number;
}
input
: { id: numberid: number }) {
return
queryOptions<Issue, IssueNotFound, IssueApi, readonly ["issue", number], undefined>(options: Omit<QueryOptions<{
    data: Issue;
    error: IssueNotFound;
    key: readonly ["issue", number];
    services: IssueApi;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>
queryOptions
({
queryKey: readonly ["issue", number]queryKey: ["issue",
input: {
    id: number;
}
input
.id: numberid],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
    data: Issue;
    error: IssueNotFound;
    key: readonly ["issue", number];
    services: IssueApi;
}>
queryFn
: () => class IssueApiIssueApi.
Service<IssueApi, { readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>; }>.use<Issue, IssueNotFound, never>(f: (service: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}) => Effect.Effect<Issue, IssueNotFound, never>): Effect.Effect<Issue, IssueNotFound, IssueApi>
use
((
api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}
api
) =>
api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound>;
}
api
.get: (id: number) => Effect.Effect<Issue, IssueNotFound>get(
input: {
    id: number;
}
input
.id: numberid)),
}); } // Effect<Issue, IssueNotFound | Query.CancelledError, QueryCache | IssueApi> const
const issue: Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, IssueApi | QueryCache.QueryCache>
issue
= import QueryCacheQueryCache.const QueryCache: Context.Service<QueryCache.QueryCache, QueryCache.QueryCache>QueryCache.
Service<QueryCache, QueryCache>.use<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, IssueApi>(f: (service: QueryCache.QueryCache) => Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, IssueApi>): Effect.Effect<...>
use
((cache: QueryCache.QueryCachecache) =>
cache: QueryCache.QueryCachecache.
QueryCache.fetch<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>, input: {
    id: number;
}): Loaded<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>> (+1 overload)
fetch
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>
issueQuery
, { id: numberid: 1 }),
); import EffectEffect.
const runFork: <QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError>(effect: Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, never>, options?: Effect.RunOptions | undefined) => Fiber<...>
runFork
(
const issue: Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, IssueApi | QueryCache.QueryCache>
issue
.
Pipeable.pipe<Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, IssueApi | QueryCache.QueryCache>, Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    services: IssueApi;
}>>>, IssueNotFound | CancelledError, IssueApi>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(import EffectEffect.
const provide: <QueryCache.QueryCache, never, never>(layer: Layer.Layer<QueryCache.QueryCache, never, never>, options?: {
    readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, QueryCache.QueryCache>> (+5 overloads)
provide
(import QueryCacheQueryCache.const layer: (config?: QueryCache.Config) => Layer.Layer<QueryCache.QueryCache>layer()), import EffectEffect.
const provide: <IssueApi, never, never>(layer: Layer.Layer<IssueApi, never, never>, options?: {
    readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, IssueApi>> (+5 overloads)
provide
(const IssueApiLive: Layer.Layer<IssueApi, never, never>IssueApiLive)),
);

Which services a request runs with

  • On QueryCache, ensure, fetch, prefetch, getQuery and watch require the factory's services, and so does Query.fetch.
  • A request runs with the services the cache was built with, under those of the latest of these calls for its entry.
  • Requests that start without a caller (a render's read, refetch, polling, focus and reconnect, invalidate) reuse the entry's last services.
  • The caller's Scope isn't passed on: a request outlives the call that started it. Scope a queryFn's resources with Effect.scoped.

Tracing

Each request runs in a raf-query.request span with a raf_query.hash attribute, under the latest caller's span if it had one. Defects are logged with Effect.logError; typed failures are the query's error.

Services in QueryClient

QueryClient gets its services from a layer. It's built into the client's scope along with the cache, so it must build synchronously:

const const queryClient: QueryClient<IssueApi>queryClient = new new QueryClient<IssueApi>(config?: QueryClientConfig<IssueApi>): QueryClient<IssueApi>QueryClient({ QueryClientConfig<IssueApi>.layer?: Layer<IssueApi, never, never> | undefinedlayer: const IssueApiLive: Layer<IssueApi, never, never>IssueApiLive }); // QueryClient<IssueApi>
const 
const handle: QueryHandle<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
= const queryClient: QueryClient<IssueApi>queryClient.
QueryClient<IssueApi>.prefetch<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, input: {
    id: number;
}): QueryHandle<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
prefetch
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
, { id: numberid: 1 });
await const queryClient: QueryClient<IssueApi>queryClient.QueryClient<IssueApi>.dispose(): Promise<void>dispose();

The client's type is QueryClient<TServices>, and its verbs accept only factories whose services the layer provides. Any other factory is a type error. dispose() interrupts every request and runs the layer's finalizers.

queryClient.runPromise(effect) runs an app's own Effect (a loader, a form action) with the cache and the layer's services, so it can yield* both:

await const queryClient: QueryClient<IssueApi>queryClient.QueryClient<IssueApi>.runPromise<void, Unauthorized | EmptyTitle>(effect: Effect.Effect<void, Unauthorized | EmptyTitle, IssueApi | QueryCache.QueryCache>): Promise<void>runPromise(
	import EffectEffect.
const gen: <Effect.Effect<Issue, Unauthorized | EmptyTitle, never> | Effect.Effect<{
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}, never, IssueApi> | Effect.Effect<...> | Effect.Effect<...>, void>(f: () => Generator<...>) => Effect.Effect<...> (+1 overload)
gen
(function* () {
const
const api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
= yield* class IssueApiIssueApi;
const const cache: QueryCache.QueryCachecache = yield* import QueryCacheQueryCache.const QueryCache: Service<QueryCache.QueryCache, QueryCache.QueryCache>QueryCache; yield*
const api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
.
rename: (input: {
    readonly id: number;
    readonly title: string;
}) => Effect.Effect<Issue, EmptyTitle | Unauthorized>
rename
({ id: numberid: 1, title: stringtitle: "Renamed" });
yield* const cache: QueryCache.QueryCachecache.QueryCache.invalidate<QueryFactory>(...target: QueryCache.Target<QueryFactory>): Effect.Effect<void>invalidate({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["issue", 1] }); }), );

Like Effect.runPromise, it rejects with the Effect's failure or defect. An Effect that needs a service the layer doesn't provide is a type error. Typed errors covers catching the failures first.

In React

QueryClientProvider takes a client with any services. A hook can't know which ones, so useQueryClient() returns a QueryClient<unknown>, whose verbs accept any factory and any Effect. A request then fails with a defect if the client's layer lacks a service it needs.

Components rarely need the services anyway. useQuery reads what a loader started, and the cache runs every request with the layer's services: the loader's, a render's, a refetch, an invalidation. So keep the check where Effects start: type the router context with the client's services, and loaders and actions get the typed client from it.

src/routes/__root.tsx
import type { class QueryClient<TServices = never>QueryClient } from "@raf-query/react";
import { function createRootRouteWithContext<TRouterContext extends {}>(): <TRegister = Register, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TSearchValidator = undefined, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TSSR = unknown, TServerMiddlewares = unknown>(options?: RootRouteOptions<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>) => RootRoute<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, unknown, unknown, TSSR, TServerMiddlewares, undefined>createRootRouteWithContext } from "@tanstack/react-router";

import type { class IssueApiIssueApi } from "~/issues.ts";

export const 
const Route: RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
Route
=
createRootRouteWithContext<{
    queryClient: QueryClient<IssueApi>;
}>(): <TRegister, TRouteContextFn, TBeforeLoadFn, TSearchValidator, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>(options?: RootRouteOptions<TRegister, TSearchValidator, {
    queryClient: QueryClient<IssueApi>;
}, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares, undefined> | undefined) => RootRoute<...>
createRootRouteWithContext
<{
queryClient: QueryClient<IssueApi>queryClient: class QueryClient<TServices = never>QueryClient<class IssueApiIssueApi>; }>()({});

Loaders start queries through the typed client, so a factory that needs a service the layer doesn't provide is a type error there. Components read the handles with useQuery, which needs no services:

src/routes/issues.$issueId.tsx
export const 
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", "/issues/$issueId", "/issues/$issueId", "/issues/$issueId", undefined, ResolveParams<"/issues/$issueId">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, ... 9 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/issues/$issueId", RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", "/issues/$issueId", "/issues/$issueId">(path?: "/issues/$issueId" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/issues/$issueId")({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient<IssueApi>; }, AnyContext, ... 7 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<(args: LoaderFnContext<Register, RootRoute<Register, undefined, ... 9 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefinedloader: (
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", ResolveParams<"/issues/$issueId">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
) =>
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", ResolveParams<"/issues/$issueId">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient<IssueApi>; }, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient<IssueApi>;
}
context
.queryClient.
QueryClient<IssueApi>.runPromise<{
    assignee: QueryHandle<(input: {
        issueId: number;
    }) => QueryOptions<Flat<{
        data: User;
        key: readonly ["issue", number, "assignee"];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}, Unauthorized | ... 1 more ... | NotFoundError>(effect: Effect.Effect<...>): Promise<...>
runPromise
(
const loadIssue: (id: number) => Effect.Effect<{
    assignee: QueryHandle<(input: {
        issueId: number;
    }) => QueryOptions<Flat<{
        data: User;
        key: readonly ["issue", number, "assignee"];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}, Unauthorized | ... 1 more ... | NotFoundError, IssueApi | QueryCache.QueryCache>
loadIssue
(
var Number: NumberConstructor
(value?: any) => number
Number
(
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", ResolveParams<"/issues/$issueId">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient<IssueApi>; }, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>.params: {
    issueId: string;
}
params
.issueId: stringissueId))),
queryClient: QueryClient<IssueApi>
UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function IssuePage(): React.JSX.ElementIssuePage, UpdatableRouteOptionsExtensions.errorComponent?: false | ErrorRouteComponent | null | undefinederrorComponent: (props: ErrorComponentPropsprops) => ( <React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.role?: React.AriaRole | undefinedrole="alert"> <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Couldn't load the issue.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={props: ErrorComponentPropsprops.reset: () => voidreset}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> </React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ), }); function function IssuePage(): React.JSX.ElementIssuePage() { const
const loaderData: {
    assignee: QueryHandle<(input: {
        issueId: number;
    }) => QueryOptions<Flat<{
        data: User;
        key: readonly ["issue", number, "assignee"];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}
loaderData
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", "/issues/$issueId", "/issues/$issueId", "/issues/$issueId", undefined, ResolveParams<"/issues/$issueId">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, ... 9 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/issues/$issueId", "/issues/$issueId">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, 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<IssueApi>;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/issues/$issueId", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
// Loader handled IssueNotFound; throw any other failure to errorComponent. const [const issue: Issueissue] =
useQuery<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, true>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, handle: QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>, options?: (Options & ... 1 more ... & {
    ...;
}) | undefined): readonly [data: ...] (+1 overload)
useQuery
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
,
const loaderData: {
    assignee: QueryHandle<(input: {
        issueId: number;
    }) => QueryOptions<Flat<{
        data: User;
        key: readonly ["issue", number, "assignee"];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}
loaderData
.
issue: QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
issue
, {
throwOnError?: true | undefinedthrowOnError: true, }); return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const issue: Issueissue.Issue.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> <
function RenameForm(props: {
    issue: Issue;
}): React.JSX.Element
RenameForm
issue: Issueissue={const issue: Issueissue} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading assignee…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function Assignee(props: {
    handle: QueryHandle<typeof assigneeQuery>;
}): React.JSX.Element
Assignee
handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
={
const loaderData: {
    assignee: QueryHandle<(input: {
        issueId: number;
    }) => QueryOptions<Flat<{
        data: User;
        key: readonly ["issue", number, "assignee"];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}
loaderData
.
assignee: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
assignee
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); }

A component that starts an Effect itself, like a form action, gets the client from the route's context too, so runPromise keeps the check:

src/components/rename-form.tsx
export function 
function RenameForm(props: {
    issue: Issue;
}): React.JSX.Element
RenameForm
(
props: {
    issue: Issue;
}
props
: { issue: Issueissue: Issue }) {
const const queryClient: QueryClient<IssueApi>queryClient =
const Route: RouteApi<"/issues/$issueId", RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>>
Route
.
RouteApi<"/issues/$issueId", RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient<IssueApi>; }, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>>.useRouteContext: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown>(opts?: UseRouteContextBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/issues/$issueId", true, unknown> | undefined) => {
    ...;
}
useRouteContext
().queryClient: QueryClient<IssueApi>queryClient; // not useQueryClient()
return ( <React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form React.FormHTMLAttributes<HTMLFormElement>.action?: string | ((formData: FormData) => void | Promise<void>) | undefinedaction={(formData: FormDataformData) => const queryClient: QueryClient<IssueApi>queryClient.QueryClient<IssueApi>.runPromise<void, never>(effect: Effect.Effect<void, never, IssueApi | QueryCache>): Promise<void>runPromise( class IssueApiIssueApi.
Service<IssueApi, { readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>; readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>; readonly rename: (input: { readonly id: number; readonly title: string; }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>; }>.use<Issue, Unauthorized | EmptyTitle, never>(f: (service: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}) => Effect.Effect<...>): Effect.Effect<...>
use
((
api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
) =>
api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
.
rename: (input: {
    readonly id: number;
    readonly title: string;
}) => Effect.Effect<Issue, EmptyTitle | Unauthorized>
rename
({
id: numberid:
props: {
    issue: Issue;
}
props
.issue: Issueissue.Issue.id: numberid,
title: stringtitle:
var String: StringConstructor
(value?: any) => string
String
(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | nullget("title")),
}), ).Pipeable.pipe<Effect.Effect<Issue, Unauthorized | EmptyTitle, IssueApi>, Effect.Effect<void, never, IssueApi>>(this: Effect.Effect<Issue, Unauthorized | EmptyTitle, IssueApi>, ab: (_: Effect.Effect<Issue, Unauthorized | EmptyTitle, IssueApi>) => Effect.Effect<void, never, IssueApi>): Effect.Effect<void, never, IssueApi> (+21 overloads)pipe(import EffectEffect.
const ignore: <Arg extends Effect.Effect<any, any, any> | {
    readonly log?: boolean | Severity | undefined;
    readonly message?: string | undefined;
} | undefined = {
    readonly log?: boolean | Severity | undefined;
    readonly message?: string | undefined;
}>(effectOrOptions?: Arg, options?: {
    readonly log?: boolean | Severity | undefined;
    readonly message?: string | undefined;
} | undefined) => [Arg] extends [Effect.Effect<infer _A, infer _E, infer _R>] ? Effect.Effect<void, never, _R> : <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<void, never, R>
ignore
),
) } > <React.JSX.IntrinsicElements.input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>input React.HTMLAttributes<HTMLInputElement>.defaultValue?: string | number | readonly string[] | undefineddefaultValue={
props: {
    issue: Issue;
}
props
.issue: Issueissue.Issue.title: stringtitle} React.InputHTMLAttributes<HTMLInputElement>.name?: string | undefinedname="title" />
</React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form> ); }

Using Effect has the whole app: service, queries, client, loader, components and action.