Typed hooks
How a data hook or composable gets its record type, what createCheloneHooks binds, and which hooks the registry never reaches.
The data hooks read the app's registered resources directly, so
useGetList("posts") is typed with no factory: the name picks the record, and a
name the app never registered is rejected at the call site. createCheloneHooks
is the other route, for an app that would rather hold the map itself. The
context hooks take neither: they read no Register and no resource name, and
their caller writes the record type.
The plain hooks
In both adapters: useGetList, useGetOne, useGetMany,
useGetManyAggregate, useGetManyReference, useCreate, useUpdate,
useDelete, useUpdateMany, useDeleteMany, useGetListLive,
useGetOneLive and useGetManyLive. Both bulk hooks build on core's
updateManyMutationOptions and deleteManyMutationOptions.
Each takes TResources and TName. TResources defaults to
RegisteredResources, which is what the Register augmentation fills
(Registering your app), and TName
defaults to keyof TResources & string. The resource argument is core's
ResourceNameArg<TResources, TName>: a name of the map for a literal, plain
string for a name the caller cannot pin, and never for a literal the map
lacks.
The two hovers below are the whole difference between a literal and a string.
const const list: UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
list = useGetList<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
useGetList("posts");
const const row: {
id: number;
title: string;
} | undefined
row = const list: UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
list.data: GetListResult<{
id: number;
title: string;
}> | undefined
The data from before the failed refetch, which is kept.
The last successfully resolved data for the query.
`undefined`, since the first fetch failed before any data was cached.
`undefined`, since the query has no data yet.
The `placeholderData` shown while the query has no data yet.data?.GetListResult<{ id: number; title: string; }>.data: {
id: number;
title: string;
}[]
data[0];
// A name read from context or props arrives as a `string`.
declare const const fromContext: stringfromContext: string;
const const loose: UseQueryResult<GetListResult<LooseRecord>, Error>loose = useGetList<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, string>(resource: string, params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<LooseRecord>, Error>
useGetList(const fromContext: stringfromContext);
const const looseRow: LooseRecord | undefinedlooseRow = const loose: UseQueryResult<GetListResult<LooseRecord>, Error>loose.data: GetListResult<LooseRecord> | undefinedThe data from before the failed refetch, which is kept.
The last successfully resolved data for the query.
`undefined`, since the first fetch failed before any data was cached.
`undefined`, since the query has no data yet.
The `placeholderData` shown while the query has no data yet.data?.GetListResult<LooseRecord>.data: LooseRecord[]data[0];
row is the post or undefined; looseRow is a LooseRecord, whose every
field is unknown. When types widen
covers what the loose arm costs.
Binding a map by hand
Every data hook still takes its map as a type argument, so a call can name one that is not the registered one:
const const typed: UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
typed = useGetList<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
useGetList<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}
resources, "posts">("posts");
const const row: {
id: number;
title: string;
} | undefined
row = const typed: UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
typed.data: GetListResult<{
id: number;
title: string;
}> | undefined
The data from before the failed refetch, which is kept.
The last successfully resolved data for the query.
`undefined`, since the first fetch failed before any data was cached.
`undefined`, since the query has no data yet.
The `placeholderData` shown while the query has no data yet.data?.GetListResult<{ id: number; title: string; }>.data: {
id: number;
title: string;
}[]
data[0];
This works, and it is clumsy: two type arguments at every call site, the map
imported into every file that calls, and a call that forgets them silently falls
back to RegisteredResources. Writing it once per hook is what the factory is
for.
createCheloneHooks
createCheloneHooks is called once, in a module of the app, and returns the
data hooks with TResources already written: the name is checked at each call
site and the record is inferred from it.
export const const hooks: {
useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<...>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks = createCheloneHooks<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}>(): {
...;
}
Bind the hooks to a registry type once — full inference at every call site:
const hooks = createCheloneHooks<typeof resources>();
hooks.useGetList('posts') // data: Post[]; 'nope' rejects
Each wrapper names the resource the hook itself names, so a bound call site
types `data` by name and an unbound `string` still reaches the loose arm.createCheloneHooks<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}
resources>();
const const bound: UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
bound = const hooks: {
useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<...>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks.useGetList: <"posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
useGetList("posts");
const const row: {
id: number;
title: string;
} | undefined
row = const bound: UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
bound.data: GetListResult<{
id: number;
title: string;
}> | undefined
The data from before the failed refetch, which is kept.
The last successfully resolved data for the query.
`undefined`, since the first fetch failed before any data was cached.
`undefined`, since the query has no data yet.
The `placeholderData` shown while the query has no data yet.data?.GetListResult<{ id: number; title: string; }>.data: {
id: number;
title: string;
}[]
data[0];
createCheloneHooks<typeof resources>() binds the map the module holds.
createCheloneHooks<RegisteredResources>() is the form that follows
Register, for an app that registers once and wants the bound hooks to read
whatever that augmentation holds.
A bound call site is stricter than a plain one. Every member constrains TName
to keyof TResources & string, so a name the caller cannot pin is rejected
instead of falling to the loose arm. For a name read from context or props, use
the plain hook, which keeps the string arm:
export const const hooks: {
useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<...>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks = createCheloneHooks<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}>(): {
...;
}
Bind the hooks to a registry type once — full inference at every call site:
const hooks = createCheloneHooks<typeof resources>();
hooks.useGetList('posts') // data: Post[]; 'nope' rejects
Each wrapper names the resource the hook itself names, so a bound call site
types `data` by name and an unbound `string` still reaches the loose arm.createCheloneHooks<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}
resources>();
declare const const fromContext: stringfromContext: string;
const hooks: {
useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<...>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks.useGetList: <"posts" | "comments">(resource: "posts" | "comments", params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<{
id: number;
title: string;
} | {
id: number;
body: string;
}>, Error>
useGetList(fromContext);What the factory returns
| Member | React | Vue |
|---|---|---|
useGetList | yes | yes |
useGetOne | yes | yes |
useGetMany | yes | yes |
useGetManyAggregate | yes | yes |
useGetManyReference | yes | yes |
useCreate | yes | yes |
useUpdate | yes | yes |
useUpdateMany | yes | yes |
useDelete | yes | yes |
useDeleteMany | yes | yes |
useInfiniteGetList is exported by both adapters and is no member of either:
its result is the infinite query shape, which the bound members do not model.
useFieldValue and the context hooks below are not members either; they take no
resource name.
Vue composables
Each Vue member forwards the composable's own signature rather than a fixed
one, so params stay MaybeRefOrGetter: a ref or a getter is a legal argument,
and a param that changes re-keys the query. data is a ref, so the read is
list.data.value.
<script setup lang="ts">
const const hooks: {
useGetList: <TName extends "posts">(resource: MaybeRefOrGetter<ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>>, params?: MaybeRefOrGetter<GetListParams>, options?: ListQueryOptions) => UseQueryReturnType<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<...>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => UseMutationReturnType<...> | Undoable<...>;
}
hooks = createCheloneHooks<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}>(): {
...;
}
Bind the composables to a registry type once:
const hooks = createCheloneHooks<typeof resources>();
hooks.useGetList('posts') // data: Post[]; 'nope' rejectscreateCheloneHooks<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}
resources>();
const const page: Ref<number, number>page = ref<number>(value: number): Ref<number, number> (+1 overload)Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref(1);
// The getter is read on every re-key, so the pagination follows `page`.
const const list: UseQueryReturnType<GetListResult<{
id: number;
title: string;
}>, Error>
list = const hooks: {
useGetList: <TName extends "posts">(resource: MaybeRefOrGetter<ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>>, params?: MaybeRefOrGetter<GetListParams>, options?: ListQueryOptions) => UseQueryReturnType<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<...>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => UseMutationReturnType<...> | Undoable<...>;
}
hooks.useGetList: <"posts">(resource: MaybeRefOrGetter<"posts">, params?: MaybeRefOrGetter<GetListParams>, options?: ListQueryOptions) => UseQueryReturnType<GetListResult<{
id: number;
title: string;
}>, Error>
useGetList("posts", () => ({
GetListParams.pagination?: Pagination | undefinedpagination: { PagePagination.page: numberpage: const page: Ref<number, number>page.Ref<number, number>.value: numbervalue, PagePagination.perPage: numberperPage: 10 },
}));
const const row: {
id: number;
title: string;
} | undefined
row = const list: UseQueryReturnType<GetListResult<{
id: number;
title: string;
}>, Error>
list.data: Ref<undefined, undefined> | Ref<GetListResult<{
id: number;
title: string;
}>, GetListResult<{
id: number;
title: string;
}>>
The data from before the failed refetch, which is kept.
The last successfully resolved data for the query.
`undefined`, since the first fetch failed before any data was cached.
`undefined`, since the query has no data yet.
The `placeholderData` shown while the query has no data yet.data.Ref<T = any, S = T>.value: GetListResult<{
id: number;
title: string;
}> | undefined
value?.GetListResult<{ id: number; title: string; }>.data: {
id: number;
title: string;
}[]
data[0];
</script>
<template>
<div: HTMLAttributes & ReservedPropsdiv>{{ const row: {
id: number;
title: string;
} & Ref<unknown, unknown> & {
value: {
id: number;
title: string;
} | undefined;
}
row?.title: string | undefinedtitle }}</div: HTMLAttributes & ReservedPropsdiv>
</template>Mutations
useUpdate and useDelete write the record core resolves for the name:
RecordOf<TResources, TName> for a name the map has, LooseRecord for a wide
string, which is what ResourceRecordArg<TResources, TName> spells out.
mutate takes { id, data, previousData }, where data is a Partial of that
record.
export const const hooks: {
useGetList: <TName extends "posts">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks = createCheloneHooks<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}>(): {
...;
}
Bind the hooks to a registry type once — full inference at every call site:
const hooks = createCheloneHooks<typeof resources>();
hooks.useGetList('posts') // data: Post[]; 'nope' rejects
Each wrapper names the resource the hook itself names, so a bound call site
types `data` by name and an unbound `string` still reaches the loose arm.createCheloneHooks<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}
resources>();
const hooks: {
useGetList: <TName extends "posts">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks.useUpdate: <"posts">(resource: "posts", options?: MutationModeOptions) => (Override<MutationObserverIdleResult<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
previous?: undefined;
key?: undefined;
manySnapshot?: undefined;
rollbackLists?: undefined;
} | {
previous: {
id: number;
title: string;
} | undefined;
key: readonly [string, "getOne", {
id: string;
meta?: undefined;
} | {
id: string;
meta: {} | null;
}];
manySnapshot: ManySnapshot<{
id: number;
title: string;
}>;
rollbackLists: () => void;
}>, {
...;
}> & {
...;
}) | ... 6 more ... | {
...;
}
useUpdate("posts").UndoableMutators<TData, TParams>.mutate: (arg0: UpdateParams<{
id: number;
title: string;
}>, arg1?: (MutateOptions<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
previous?: undefined;
key?: undefined;
manySnapshot?: undefined;
rollbackLists?: undefined;
} | {
previous: {
id: number;
title: string;
} | undefined;
key: readonly [string, "getOne", {
id: string;
meta?: undefined;
} | {
id: string;
meta: {} | null;
}];
manySnapshot: ManySnapshot<{
id: number;
title: string;
}>;
rollbackLists: () => void;
}> & UndoableCallbacks<{
id: number;
title: string;
}, UpdateParams<...>>) | undefined) => void
The mutation function you can call with variables to trigger the mutation and optionally hooks on additional callback options.mutate({ UpdateParams<TRecord extends ResourceRecord = LooseRecord>.id: Identifierid: 1, UpdateParams<{ id: number; title: string; }>.data: Partial<{
id: number;
title: string;
}>
data: { title: 1 } });With mutationMode: "undoable" the adapter swaps in the pair core builds
(undoableUpdateMutators and undoableDeleteMutators), which patches the cache
now and defers the provider call. That pair is fire-and-forget: its
mutateAsync calls mutate and settles at once, so in the undoable mode it
resolves to nothing every time, undo or no undo. The Post | void on
mutateAsync is the union of the undoable return and the ordinary one, because
the adapter picks the shape from the mode it is handed at runtime and the type
cannot see which one a call got. Await the record only in the ordinary modes:
const const update: (Override<MutationObserverIdleResult<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
previous?: undefined;
key?: undefined;
manySnapshot?: undefined;
rollbackLists?: undefined;
} | {
previous: {
id: number;
title: string;
} | undefined;
key: readonly [string, "getOne", {
id: string;
meta?: undefined;
} | {
id: string;
meta: {} | null;
}];
manySnapshot: ManySnapshot<{
id: number;
title: string;
}>;
rollbackLists: () => void;
}>, {
mutate: UseMutateFunction<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
...;
} | {
...;
}>;
}> & {
...;
}) | ... 6 more ... | {
...;
}
update = useUpdate<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "posts">(resource: "posts", options?: MutationModeOptions): (Override<MutationObserverIdleResult<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
previous?: undefined;
key?: undefined;
manySnapshot?: undefined;
rollbackLists?: undefined;
} | {
previous: {
id: number;
title: string;
} | undefined;
key: readonly [string, "getOne", {
id: string;
meta?: undefined;
} | {
id: string;
meta: {} | null;
}];
manySnapshot: ManySnapshot<...>;
rollbackLists: () => void;
}>, {
...;
}> & {
...;
}) | ... 6 more ... | {
...;
}
useUpdate("posts", { MutationModeOptions.mutationMode?: MutationMode | undefinedmutationMode: "undoable" });
// The undoable pair settles once the undoable is enqueued.
const const done: Promise<void> | Promise<{
id: number;
title: string;
}>
done = const update: (Override<MutationObserverIdleResult<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
previous?: undefined;
key?: undefined;
manySnapshot?: undefined;
rollbackLists?: undefined;
} | {
previous: {
id: number;
title: string;
} | undefined;
key: readonly [string, "getOne", {
id: string;
meta?: undefined;
} | {
id: string;
meta: {} | null;
}];
manySnapshot: ManySnapshot<{
id: number;
title: string;
}>;
rollbackLists: () => void;
}>, {
mutate: UseMutateFunction<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
...;
} | {
...;
}>;
}> & {
...;
}) | ... 6 more ... | {
...;
}
update.mutateAsync: (variables: UpdateParams<{
id: number;
title: string;
}>, options?: MutateOptions<{
id: number;
title: string;
}, Error, UpdateParams<{
id: number;
title: string;
}>, {
previous?: undefined;
key?: undefined;
manySnapshot?: undefined;
rollbackLists?: undefined;
} | {
previous: {
id: number;
title: string;
} | undefined;
key: readonly [string, "getOne", {
id: string;
meta?: undefined;
} | {
id: string;
meta: {} | null;
}];
manySnapshot: ManySnapshot<{
id: number;
title: string;
}>;
rollbackLists: () => void;
}> | undefined) => Promise<...> | Promise<...> (+1 overload)
mutateAsync({ UpdateParams<TRecord extends ResourceRecord = LooseRecord>.id: Identifierid: 1, UpdateParams<{ id: number; title: string; }>.data: Partial<{
id: number;
title: string;
}>
data: { title?: string | undefinedtitle: "new" } });
packages/react/src/hooks.test-d.ts pins the type rather than the arrival: it
asserts mutateAsync equals Post | void, and that assigning it to a Post is
an error.
The record context hooks
useRecordContext<RecordType = LooseRecord> in both adapters,
useRecordContextRef in Vue (a ComputedRef, since the ambient record can
resolve after setup), and useListContext in both. React adds
useEditContext, useShowContext and useCreateContext; Vue has no view
contexts yet.
None of them reads Register, and none takes a resource name. The caller names
the record:
const const record: RecordContextValue<{
id: number;
title: string;
}>
record = useRecordContext<{
id: number;
title: string;
}>(props?: UseRecordContextOptions<{
id: number;
title: string;
}> | undefined): RecordContextValue<{
id: number;
title: string;
}>
Read the ambient record. A `record` prop always wins over the context value
(parity: ra-core's `useRecordContext`), so a caller can target a specific
record inline. Returns `undefined` when neither a prop nor an ancestor
provider supplies one.useRecordContext<type RegisteredRecord<TName extends RegisteredResourceName> = {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}[TName] extends {
schema: infer S;
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord
The record type of a registered resource, or
{@link
LooseRecord
}
when it has no schema.RegisteredRecord<"posts">>();
const const title: string | undefinedtitle = const record: RecordContextValue<{
id: number;
title: string;
}>
record?.title: string | undefinedtitle;
RecordContextValue<RecordType> is RecordType | undefined. Vue's
RecordValue<RecordType> is the same union, and it is not a ref:
useRecordContext returns the value itself, and useRecordContextRef returns
it inside a ComputedRef.
The type is a claim about the provider above it, not a fact read off it: a
context value cannot carry a type argument, so the provider publishes the record
at LooseRecord and the hook reads it back at whatever the caller wrote. Each
adapter marks that read-back with a type-escape comment where the cast sits:
useRecordContextRef and useListContext in Vue, useListContext and the edit
and create context providers in React. A wrong RecordType here is a wrong
claim, and nothing in the type system argues with it.
useFieldValue
useFieldValue<RecordType, TPath> reads the value at a source path off the
ambient record. A typed record and a literal source give the value's type;
with no record the path is loose and the value is unknown.
const const post: RecordContextValue<{
id: number;
title: string;
}>
post = useRecordContext<{
id: number;
title: string;
}>(props?: UseRecordContextOptions<{
id: number;
title: string;
}> | undefined): RecordContextValue<{
id: number;
title: string;
}>
Read the ambient record. A `record` prop always wins over the context value
(parity: ra-core's `useRecordContext`), so a caller can target a specific
record inline. Returns `undefined` when neither a prop nor an ancestor
provider supplies one.useRecordContext<type RegisteredRecord<TName extends RegisteredResourceName> = {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}[TName] extends {
schema: infer S;
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord
The record type of a registered resource, or
{@link
LooseRecord
}
when it has no schema.RegisteredRecord<"posts">>();
const const title: stringtitle = useFieldValue<{
id: number;
title: string;
}, "title">(params: UseFieldValueOptions<{
id: number;
title: string;
}, "title">): string
Get the value of a field of the current record. A typed `record` and a
literal `source` give the value's type; `useFieldValue<Post>(...)` alone
infers no path, so the value is the union of Post's value types.
The type does not include `undefined` for a record that has not loaded: before
the ambient record resolves the read is `undefined`, so guard it in a view that
renders while loading.useFieldValue({ UseFieldValueOptions<{ id: number; title: string; }, "title">.source: "title"The field path — dot members and/or bracket/numeric indices.source: "title", UseRecordContextOptions<{ id: number; title: string; }>.record?: RecordContextValue<{
id: number;
title: string;
}>
record: const post: RecordContextValue<{
id: number;
title: string;
}>
post });
const const looseTitle: unknownlooseTitle = useFieldValue<LooseRecord, "title">(params: UseFieldValueOptions<LooseRecord, "title">): unknownGet the value of a field of the current record. A typed `record` and a
literal `source` give the value's type; `useFieldValue<Post>(...)` alone
infers no path, so the value is the union of Post's value types.
The type does not include `undefined` for a record that has not loaded: before
the ambient record resolves the read is `undefined`, so guard it in a view that
renders while loading.useFieldValue({ UseFieldValueOptions<LooseRecord, "title">.source: "title"The field path — dot members and/or bracket/numeric indices.source: "title" });
The type does not include undefined for a record that has not loaded. Before
the ambient record resolves the read is undefined, so a view that renders
while loading guards it. In Vue the same read returns a ComputedRef, which
re-renders when the record arrives.
A name the registry lacks
export const const hooks: {
useGetList: <TName extends "posts">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks = createCheloneHooks<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}>(): {
...;
}
Bind the hooks to a registry type once — full inference at every call site:
const hooks = createCheloneHooks<typeof resources>();
hooks.useGetList('posts') // data: Post[]; 'nope' rejects
Each wrapper names the resource the hook itself names, so a bound call site
types `data` by name and an unbound `string` still reaches the loose arm.createCheloneHooks<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}
resources>();
const hooks: {
useGetList: <TName extends "posts">(resource: ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, ResourceNameArg<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, TName>>>, Error>;
... 8 more ...;
useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
...;
}) | ... 6 more ... | {
...;
};
}
hooks.useGetList: <"posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<{
id: number;
title: string;
}>, Error>
useGetList("nope");
// The plain hook rejects it too, at `never`.
useGetList<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "psots">(resource: never, params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<LooseRecord>, Error>
useGetList("psots");The bound calls are pinned in packages/vue/src/data/create-chelone-hooks.test-d.ts
and packages/react/src/hooks.test.tsx. The plain hooks rest on
ResourceNameArg, whose own arms are pinned in
packages/core/src/resource/register.test-d.ts.
Where to read next
- Registering your app for
defineResourcesand theRegisteraugmentation the hooks read. - Typed components for the same registry
behind a component's props, and
createResourceComponents. - Sources, filters and sort for
the path grammar a
sourcetakes and the operator keys a filter writes. - Translations and permissions
for
useTranslateandusePermissions, which register the same way. - When types widen for
LooseRecordand every other fallback.