@simplix-react/react 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/dist/index.d.ts +245 -12
- package/dist/index.js +99 -8
- package/package.json +8 -2
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# @simplix-react/react
|
|
2
|
+
|
|
3
|
+
Type-safe React Query hooks derived automatically from an `@simplix-react/contract` API contract.
|
|
4
|
+
|
|
5
|
+
> **Prerequisites:** Requires a contract defined with `@simplix-react/contract`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @simplix-react/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Peer dependencies:**
|
|
14
|
+
|
|
15
|
+
| Package | Version |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `@simplix-react/contract` | workspace |
|
|
18
|
+
| `@tanstack/react-query` | >= 5.0.0 |
|
|
19
|
+
| `react` | >= 18.0.0 |
|
|
20
|
+
| `zod` | >= 4.0.0 |
|
|
21
|
+
|
|
22
|
+
## Quick Example
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { defineApi } from "@simplix-react/contract";
|
|
26
|
+
import { deriveHooks } from "@simplix-react/react";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
|
|
29
|
+
// 1. Define the contract
|
|
30
|
+
const projectContract = defineApi({
|
|
31
|
+
domain: "project",
|
|
32
|
+
basePath: "/api",
|
|
33
|
+
entities: {
|
|
34
|
+
task: {
|
|
35
|
+
path: "/tasks",
|
|
36
|
+
schema: z.object({ id: z.string(), title: z.string(), status: z.string() }),
|
|
37
|
+
createSchema: z.object({ title: z.string(), status: z.string() }),
|
|
38
|
+
updateSchema: z.object({ title: z.string().optional(), status: z.string().optional() }),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 2. Derive hooks — one call generates everything
|
|
44
|
+
const hooks = deriveHooks(projectContract);
|
|
45
|
+
|
|
46
|
+
// 3. Use in components
|
|
47
|
+
function TaskList() {
|
|
48
|
+
const { data: tasks, isLoading } = hooks.task.useList();
|
|
49
|
+
const createTask = hooks.task.useCreate();
|
|
50
|
+
|
|
51
|
+
if (isLoading) return <p>Loading...</p>;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ul>
|
|
55
|
+
{tasks?.map((task) => (
|
|
56
|
+
<li key={task.id}>{task.title}</li>
|
|
57
|
+
))}
|
|
58
|
+
</ul>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API Overview
|
|
64
|
+
|
|
65
|
+
The package exports a single function and a set of type definitions:
|
|
66
|
+
|
|
67
|
+
| Export | Kind | Description |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `deriveHooks` | Function | Derives all hooks from a contract |
|
|
70
|
+
| `EntityHooks` | Type | Hook interface for a single entity |
|
|
71
|
+
| `OperationHooks` | Type | Hook interface for a custom operation |
|
|
72
|
+
| `DerivedListHook` | Type | List query hook signature |
|
|
73
|
+
| `DerivedGetHook` | Type | Detail query hook signature |
|
|
74
|
+
| `DerivedCreateHook` | Type | Create mutation hook signature |
|
|
75
|
+
| `DerivedUpdateHook` | Type | Update mutation hook signature |
|
|
76
|
+
| `DerivedDeleteHook` | Type | Delete mutation hook signature |
|
|
77
|
+
| `DerivedInfiniteListHook` | Type | Infinite list query hook signature |
|
|
78
|
+
| `OperationMutationHook` | Type | Operation mutation hook signature |
|
|
79
|
+
|
|
80
|
+
## Key Concepts
|
|
81
|
+
|
|
82
|
+
### Hook Derivation
|
|
83
|
+
|
|
84
|
+
`deriveHooks()` reads the entity and operation definitions from a contract and generates a typed hook object. Each entity key maps to an `EntityHooks` object, and each operation key maps to an `OperationHooks` object.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const hooks = deriveHooks(projectContract);
|
|
88
|
+
// hooks.task → EntityHooks<TaskSchema, CreateTaskSchema, UpdateTaskSchema>
|
|
89
|
+
// hooks.archiveProject → OperationHooks<ArchiveInput, ArchiveOutput>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Auto-Invalidation
|
|
93
|
+
|
|
94
|
+
Mutation hooks automatically invalidate related queries:
|
|
95
|
+
|
|
96
|
+
- **Entity mutations** (`useCreate`, `useUpdate`, `useDelete`) invalidate all queries under the entity's query key scope.
|
|
97
|
+
- **Operation mutations** invalidate based on the `invalidates` function defined in the operation's contract configuration.
|
|
98
|
+
|
|
99
|
+
No manual `queryClient.invalidateQueries()` calls are needed.
|
|
100
|
+
|
|
101
|
+
### TanStack Query Options Passthrough
|
|
102
|
+
|
|
103
|
+
All hooks accept TanStack Query options as their last argument. Query hooks accept all `UseQueryOptions` except `queryKey` and `queryFn`. Mutation hooks accept all `UseMutationOptions` except `mutationFn`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Pass query options
|
|
107
|
+
const { data } = hooks.task.useList({ enabled: false });
|
|
108
|
+
|
|
109
|
+
// Pass mutation options
|
|
110
|
+
const createTask = hooks.task.useCreate(undefined, {
|
|
111
|
+
onSuccess: (data) => console.log("Created:", data),
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Hook Reference
|
|
116
|
+
|
|
117
|
+
### `useList`
|
|
118
|
+
|
|
119
|
+
Fetches a list of entities. Supports three calling conventions:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// Top-level entity
|
|
123
|
+
hooks.task.useList();
|
|
124
|
+
hooks.task.useList({ enabled: isReady });
|
|
125
|
+
|
|
126
|
+
// With filters/sort
|
|
127
|
+
hooks.task.useList({
|
|
128
|
+
filters: { status: "open" },
|
|
129
|
+
sort: { field: "createdAt", direction: "desc" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Child entity with parent ID
|
|
133
|
+
hooks.task.useList(projectId);
|
|
134
|
+
hooks.task.useList(projectId, { filters: { status: "open" } });
|
|
135
|
+
hooks.task.useList(projectId, { filters: { status: "open" } }, { enabled: isReady });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For child entities, the query is automatically disabled when `parentId` is falsy.
|
|
139
|
+
|
|
140
|
+
### `useGet`
|
|
141
|
+
|
|
142
|
+
Fetches a single entity by ID.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const { data: task } = hooks.task.useGet(taskId);
|
|
146
|
+
const { data: task } = hooks.task.useGet(taskId, { staleTime: 5000 });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The query is automatically disabled when `id` is falsy.
|
|
150
|
+
|
|
151
|
+
### `useCreate`
|
|
152
|
+
|
|
153
|
+
Creates a new entity. For child entities, pass the parent ID.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// Top-level entity
|
|
157
|
+
const createTask = hooks.task.useCreate();
|
|
158
|
+
createTask.mutate({ title: "New task", status: "open" });
|
|
159
|
+
|
|
160
|
+
// Child entity
|
|
161
|
+
const createTask = hooks.task.useCreate(projectId);
|
|
162
|
+
createTask.mutate({ title: "New task", status: "open" });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `useUpdate`
|
|
166
|
+
|
|
167
|
+
Updates an existing entity. Supports optimistic updates.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// Standard update
|
|
171
|
+
const updateTask = hooks.task.useUpdate();
|
|
172
|
+
updateTask.mutate({ id: taskId, dto: { status: "done" } });
|
|
173
|
+
|
|
174
|
+
// Optimistic update — UI updates instantly, rolls back on error
|
|
175
|
+
const updateTask = hooks.task.useUpdate({ optimistic: true });
|
|
176
|
+
updateTask.mutate({ id: taskId, dto: { status: "done" } });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `useDelete`
|
|
180
|
+
|
|
181
|
+
Deletes an entity by ID.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const deleteTask = hooks.task.useDelete();
|
|
185
|
+
deleteTask.mutate(taskId);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `useInfiniteList`
|
|
189
|
+
|
|
190
|
+
Fetches paginated data with cursor-based or offset-based pagination. Pagination is managed automatically based on the response's `meta` field.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const {
|
|
194
|
+
data,
|
|
195
|
+
fetchNextPage,
|
|
196
|
+
hasNextPage,
|
|
197
|
+
isFetchingNextPage,
|
|
198
|
+
} = hooks.task.useInfiniteList(projectId, {
|
|
199
|
+
limit: 10,
|
|
200
|
+
filters: { status: "open" },
|
|
201
|
+
sort: { field: "createdAt", direction: "desc" },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Access flattened data
|
|
205
|
+
const allTasks = data?.pages.flatMap((page) => page.data) ?? [];
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Operation `useMutation`
|
|
209
|
+
|
|
210
|
+
Custom operations defined in the contract each produce a `useMutation` hook.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const archiveProject = hooks.archiveProject.useMutation({
|
|
214
|
+
onSuccess: () => {
|
|
215
|
+
// Cache invalidation is already handled via the contract's `invalidates`
|
|
216
|
+
console.log("Project archived");
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
archiveProject.mutate({ projectId: "abc" });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Related Packages
|
|
224
|
+
|
|
225
|
+
| Package | Description |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `@simplix-react/contract` | Define type-safe API contracts |
|
|
228
|
+
| `@simplix-react/mock` | Generate MSW handlers from contracts for testing |
|
|
229
|
+
| `@simplix-react/i18n` | i18next-based internationalization framework |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
Next Step → `@simplix-react/mock`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,93 @@
|
|
|
1
|
-
import { EntityDefinition, OperationDefinition, ApiContractConfig, QueryKeyFactory } from '@simplix-react/contract';
|
|
1
|
+
import { ListParams, PageInfo, EntityDefinition, OperationDefinition, ApiContractConfig, QueryKeyFactory } from '@simplix-react/contract';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
|
3
|
+
import { UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult, UseInfiniteQueryResult } from '@tanstack/react-query';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Represents a derived list query hook with overloaded call signatures.
|
|
7
|
+
*
|
|
8
|
+
* Supports three calling conventions:
|
|
9
|
+
* - `useList(options?)` — top-level entity list
|
|
10
|
+
* - `useList(params, options?)` — filtered/sorted list
|
|
11
|
+
* - `useList(parentId, params?, options?)` — child entity list
|
|
12
|
+
*
|
|
13
|
+
* All TanStack Query options except `queryKey` and `queryFn` can be passed through.
|
|
14
|
+
*
|
|
15
|
+
* @typeParam TData - The entity type returned by the query
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
20
|
+
*
|
|
21
|
+
* const hooks = deriveHooks(projectContract);
|
|
22
|
+
* const { data: tasks } = hooks.task.useList(projectId, {
|
|
23
|
+
* filters: { status: "open" },
|
|
24
|
+
* sort: { field: "createdAt", direction: "desc" },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @see {@link EntityHooks} for the complete set of entity hooks.
|
|
29
|
+
*/
|
|
30
|
+
type DerivedListHook<TData> = (parentIdOrParams?: string | ListParams, paramsOrOptions?: ListParams | Omit<UseQueryOptions<TData[], Error>, "queryKey" | "queryFn">, options?: Omit<UseQueryOptions<TData[], Error>, "queryKey" | "queryFn">) => UseQueryResult<TData[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Represents a derived detail query hook that fetches a single entity by ID.
|
|
33
|
+
*
|
|
34
|
+
* Automatically disables the query when `id` is falsy.
|
|
35
|
+
*
|
|
36
|
+
* @typeParam TData - The entity type returned by the query
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
41
|
+
*
|
|
42
|
+
* const hooks = deriveHooks(projectContract);
|
|
43
|
+
* const { data: task } = hooks.task.useGet(taskId);
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @see {@link EntityHooks} for the complete set of entity hooks.
|
|
7
47
|
*/
|
|
8
|
-
type DerivedListHook<TData> = (parentId: string, options?: Omit<UseQueryOptions<TData[], Error>, "queryKey" | "queryFn">) => UseQueryResult<TData[]>;
|
|
9
48
|
type DerivedGetHook<TData> = (id: string, options?: Omit<UseQueryOptions<TData, Error>, "queryKey" | "queryFn">) => UseQueryResult<TData>;
|
|
49
|
+
/**
|
|
50
|
+
* Represents a derived create mutation hook.
|
|
51
|
+
*
|
|
52
|
+
* Automatically invalidates all entity queries on success.
|
|
53
|
+
* For child entities, accepts a `parentId` as the first argument.
|
|
54
|
+
*
|
|
55
|
+
* @typeParam TInput - The create DTO type (inferred from the entity's createSchema)
|
|
56
|
+
* @typeParam TOutput - The entity type returned after creation
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
61
|
+
*
|
|
62
|
+
* const hooks = deriveHooks(projectContract);
|
|
63
|
+
* const createTask = hooks.task.useCreate(projectId);
|
|
64
|
+
* createTask.mutate({ title: "New task", status: "open" });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @see {@link EntityHooks} for the complete set of entity hooks.
|
|
68
|
+
*/
|
|
10
69
|
type DerivedCreateHook<TInput, TOutput> = (parentId?: string, options?: Omit<UseMutationOptions<TOutput, Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, Error, TInput>;
|
|
70
|
+
/**
|
|
71
|
+
* Represents a derived update mutation hook.
|
|
72
|
+
*
|
|
73
|
+
* Accepts `{ id, dto }` as mutation variables. Supports optimistic updates
|
|
74
|
+
* via the `optimistic` option. Automatically invalidates all entity queries
|
|
75
|
+
* on settlement.
|
|
76
|
+
*
|
|
77
|
+
* @typeParam TInput - The update DTO type (inferred from the entity's updateSchema)
|
|
78
|
+
* @typeParam TOutput - The entity type returned after update
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
83
|
+
*
|
|
84
|
+
* const hooks = deriveHooks(projectContract);
|
|
85
|
+
* const updateTask = hooks.task.useUpdate({ optimistic: true });
|
|
86
|
+
* updateTask.mutate({ id: taskId, dto: { status: "done" } });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @see {@link EntityHooks} for the complete set of entity hooks.
|
|
90
|
+
*/
|
|
11
91
|
type DerivedUpdateHook<TInput, TOutput> = (options?: Omit<UseMutationOptions<TOutput, Error, {
|
|
12
92
|
id: string;
|
|
13
93
|
dto: TInput;
|
|
@@ -15,9 +95,78 @@ type DerivedUpdateHook<TInput, TOutput> = (options?: Omit<UseMutationOptions<TOu
|
|
|
15
95
|
id: string;
|
|
16
96
|
dto: TInput;
|
|
17
97
|
}>;
|
|
98
|
+
/**
|
|
99
|
+
* Represents a derived delete mutation hook.
|
|
100
|
+
*
|
|
101
|
+
* Accepts the entity ID as the mutation variable. Automatically invalidates
|
|
102
|
+
* all entity queries on success.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
107
|
+
*
|
|
108
|
+
* const hooks = deriveHooks(projectContract);
|
|
109
|
+
* const deleteTask = hooks.task.useDelete();
|
|
110
|
+
* deleteTask.mutate(taskId);
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @see {@link EntityHooks} for the complete set of entity hooks.
|
|
114
|
+
*/
|
|
18
115
|
type DerivedDeleteHook = (options?: Omit<UseMutationOptions<void, Error, string>, "mutationFn">) => UseMutationResult<void, Error, string>;
|
|
19
116
|
/**
|
|
20
|
-
*
|
|
117
|
+
* Represents a derived infinite list query hook for cursor-based or offset-based pagination.
|
|
118
|
+
*
|
|
119
|
+
* Automatically determines the next page parameter from the response `meta` field.
|
|
120
|
+
* Pagination parameters are managed internally; callers provide only filters, sort,
|
|
121
|
+
* and an optional page size limit.
|
|
122
|
+
*
|
|
123
|
+
* @typeParam TData - The entity type returned in each page
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
128
|
+
*
|
|
129
|
+
* const hooks = deriveHooks(projectContract);
|
|
130
|
+
* const { data, fetchNextPage, hasNextPage } = hooks.task.useInfiniteList(
|
|
131
|
+
* projectId,
|
|
132
|
+
* { limit: 10, filters: { status: "open" } },
|
|
133
|
+
* );
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* @see {@link EntityHooks} for the complete set of entity hooks.
|
|
137
|
+
*/
|
|
138
|
+
type DerivedInfiniteListHook<TData> = (parentId?: string, params?: Omit<ListParams, "pagination"> & {
|
|
139
|
+
limit?: number;
|
|
140
|
+
}, options?: Record<string, unknown>) => UseInfiniteQueryResult<{
|
|
141
|
+
data: TData[];
|
|
142
|
+
meta: PageInfo;
|
|
143
|
+
}, Error>;
|
|
144
|
+
/**
|
|
145
|
+
* Represents the complete set of React Query hooks derived from an entity definition.
|
|
146
|
+
*
|
|
147
|
+
* Each entity in the contract produces an object conforming to this interface,
|
|
148
|
+
* with hooks for CRUD operations and infinite scrolling.
|
|
149
|
+
*
|
|
150
|
+
* @typeParam TSchema - The Zod schema defining the entity shape
|
|
151
|
+
* @typeParam TCreate - The Zod schema defining the create DTO shape
|
|
152
|
+
* @typeParam TUpdate - The Zod schema defining the update DTO shape
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
157
|
+
*
|
|
158
|
+
* const hooks = deriveHooks(projectContract);
|
|
159
|
+
*
|
|
160
|
+
* // hooks.task satisfies EntityHooks<TaskSchema, CreateTaskSchema, UpdateTaskSchema>
|
|
161
|
+
* const { data } = hooks.task.useList();
|
|
162
|
+
* const { data: single } = hooks.task.useGet(id);
|
|
163
|
+
* const create = hooks.task.useCreate();
|
|
164
|
+
* const update = hooks.task.useUpdate();
|
|
165
|
+
* const remove = hooks.task.useDelete();
|
|
166
|
+
* const infinite = hooks.task.useInfiniteList();
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @see {@link deriveHooks} for generating these hooks from a contract.
|
|
21
170
|
*/
|
|
22
171
|
interface EntityHooks<TSchema extends z.ZodTypeAny, TCreate extends z.ZodTypeAny, TUpdate extends z.ZodTypeAny> {
|
|
23
172
|
useList: DerivedListHook<z.infer<TSchema>>;
|
|
@@ -25,11 +174,51 @@ interface EntityHooks<TSchema extends z.ZodTypeAny, TCreate extends z.ZodTypeAny
|
|
|
25
174
|
useCreate: DerivedCreateHook<z.infer<TCreate>, z.infer<TSchema>>;
|
|
26
175
|
useUpdate: DerivedUpdateHook<z.infer<TUpdate>, z.infer<TSchema>>;
|
|
27
176
|
useDelete: DerivedDeleteHook;
|
|
177
|
+
useInfiniteList: DerivedInfiniteListHook<z.infer<TSchema>>;
|
|
28
178
|
}
|
|
29
179
|
/**
|
|
30
|
-
*
|
|
180
|
+
* Represents a derived mutation hook for a custom operation.
|
|
181
|
+
*
|
|
182
|
+
* Wraps the operation client function with TanStack Query's `useMutation`.
|
|
183
|
+
* All mutation options except `mutationFn` can be passed through.
|
|
184
|
+
*
|
|
185
|
+
* @typeParam TInput - The input type for the operation
|
|
186
|
+
* @typeParam TOutput - The output type for the operation
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
191
|
+
*
|
|
192
|
+
* const hooks = deriveHooks(projectContract);
|
|
193
|
+
* const archiveProject = hooks.archiveProject.useMutation();
|
|
194
|
+
* archiveProject.mutate({ projectId: "abc" });
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @see {@link OperationHooks} for the operation hooks container.
|
|
31
198
|
*/
|
|
32
199
|
type OperationMutationHook<TInput, TOutput> = (options?: Omit<UseMutationOptions<TOutput, Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, Error, TInput>;
|
|
200
|
+
/**
|
|
201
|
+
* Represents the hook container for a custom operation defined in the contract.
|
|
202
|
+
*
|
|
203
|
+
* Each operation in the contract produces an object with a single `useMutation` hook.
|
|
204
|
+
* Cache invalidation is handled automatically based on the operation's `invalidates`
|
|
205
|
+
* configuration in the contract.
|
|
206
|
+
*
|
|
207
|
+
* @typeParam TInput - The Zod schema defining the operation input
|
|
208
|
+
* @typeParam TOutput - The Zod schema defining the operation output
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
213
|
+
*
|
|
214
|
+
* const hooks = deriveHooks(projectContract);
|
|
215
|
+
* const { mutate, isPending } = hooks.archiveProject.useMutation({
|
|
216
|
+
* onSuccess: () => console.log("Project archived"),
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @see {@link deriveHooks} for generating hooks from a contract.
|
|
221
|
+
*/
|
|
33
222
|
interface OperationHooks<TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny> {
|
|
34
223
|
useMutation: OperationMutationHook<z.infer<TInput>, z.infer<TOutput>>;
|
|
35
224
|
}
|
|
@@ -37,13 +226,57 @@ interface OperationHooks<TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeA
|
|
|
37
226
|
type AnyEntityDef = EntityDefinition<z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny>;
|
|
38
227
|
type AnyOperationDef = OperationDefinition<z.ZodTypeAny, z.ZodTypeAny>;
|
|
39
228
|
/**
|
|
40
|
-
*
|
|
229
|
+
* Derives type-safe React Query hooks from an API contract.
|
|
230
|
+
*
|
|
231
|
+
* Generates a complete set of hooks for every entity and operation defined in
|
|
232
|
+
* the contract. Entity hooks include `useList`, `useGet`, `useCreate`,
|
|
233
|
+
* `useUpdate`, `useDelete`, and `useInfiniteList`. Operation hooks provide
|
|
234
|
+
* a single `useMutation` with automatic cache invalidation.
|
|
235
|
+
*
|
|
236
|
+
* All hooks support full TanStack Query options passthrough — callers can
|
|
237
|
+
* provide any option except `queryKey`/`queryFn` (for queries) or
|
|
238
|
+
* `mutationFn` (for mutations), which are managed internally.
|
|
239
|
+
*
|
|
240
|
+
* @typeParam TEntities - Record of entity definitions from the contract
|
|
241
|
+
* @typeParam TOperations - Record of operation definitions from the contract
|
|
242
|
+
*
|
|
243
|
+
* @param contract - The API contract produced by `defineApi()` from `@simplix-react/contract`,
|
|
244
|
+
* containing `config`, `client`, and `queryKeys`.
|
|
245
|
+
*
|
|
246
|
+
* @returns An object keyed by entity/operation name, each containing its derived hooks.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* import { defineApi } from "@simplix-react/contract";
|
|
251
|
+
* import { deriveHooks } from "@simplix-react/react";
|
|
252
|
+
* import { z } from "zod";
|
|
253
|
+
*
|
|
254
|
+
* const projectContract = defineApi({
|
|
255
|
+
* domain: "project",
|
|
256
|
+
* basePath: "/api",
|
|
257
|
+
* entities: {
|
|
258
|
+
* task: {
|
|
259
|
+
* path: "/tasks",
|
|
260
|
+
* schema: z.object({ id: z.string(), title: z.string(), status: z.string() }),
|
|
261
|
+
* createSchema: z.object({ title: z.string(), status: z.string() }),
|
|
262
|
+
* updateSchema: z.object({ title: z.string().optional(), status: z.string().optional() }),
|
|
263
|
+
* },
|
|
264
|
+
* },
|
|
265
|
+
* });
|
|
266
|
+
*
|
|
267
|
+
* // Derive all hooks at once
|
|
268
|
+
* const hooks = deriveHooks(projectContract);
|
|
41
269
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
270
|
+
* // Use in components
|
|
271
|
+
* function TaskList() {
|
|
272
|
+
* const { data: tasks } = hooks.task.useList();
|
|
273
|
+
* const createTask = hooks.task.useCreate();
|
|
274
|
+
* // ...
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
44
277
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
278
|
+
* @see {@link EntityHooks} for the per-entity hook interface.
|
|
279
|
+
* @see {@link OperationHooks} for the per-operation hook interface.
|
|
47
280
|
*/
|
|
48
281
|
declare function deriveHooks<TEntities extends Record<string, AnyEntityDef>, TOperations extends Record<string, AnyOperationDef>>(contract: {
|
|
49
282
|
config: ApiContractConfig<TEntities, TOperations>;
|
|
@@ -56,4 +289,4 @@ type DerivedHooksResult<TEntities extends Record<string, AnyEntityDef>, TOperati
|
|
|
56
289
|
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput> ? OperationHooks<TInput, TOutput> : never;
|
|
57
290
|
};
|
|
58
291
|
|
|
59
|
-
export { type DerivedCreateHook, type DerivedDeleteHook, type DerivedGetHook, type DerivedListHook, type DerivedUpdateHook, type EntityHooks, type OperationHooks, type OperationMutationHook, deriveHooks };
|
|
292
|
+
export { type DerivedCreateHook, type DerivedDeleteHook, type DerivedGetHook, type DerivedInfiniteListHook, type DerivedListHook, type DerivedUpdateHook, type EntityHooks, type OperationHooks, type OperationMutationHook, deriveHooks };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
|
|
1
|
+
import { useInfiniteQuery, useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
|
|
2
2
|
|
|
3
3
|
// src/derive-hooks.ts
|
|
4
4
|
function deriveHooks(contract) {
|
|
@@ -19,14 +19,27 @@ function deriveHooks(contract) {
|
|
|
19
19
|
}
|
|
20
20
|
return result;
|
|
21
21
|
}
|
|
22
|
-
function createEntityHooks(entity, entityClient, keys,
|
|
22
|
+
function createEntityHooks(entity, entityClient, keys, _allQueryKeys, _config) {
|
|
23
23
|
return {
|
|
24
|
-
useList(
|
|
24
|
+
useList(parentIdOrParams, paramsOrOptions, options) {
|
|
25
|
+
const { parentId, listParams, queryOptions } = resolveListArgs(
|
|
26
|
+
parentIdOrParams,
|
|
27
|
+
paramsOrOptions,
|
|
28
|
+
options
|
|
29
|
+
);
|
|
30
|
+
const keyParams = {};
|
|
31
|
+
if (entity.parent && parentId) keyParams[entity.parent.param] = parentId;
|
|
32
|
+
if (listParams) Object.assign(keyParams, listParams);
|
|
25
33
|
return useQuery({
|
|
26
|
-
queryKey: keys.list(
|
|
27
|
-
queryFn: () =>
|
|
34
|
+
queryKey: keys.list(keyParams),
|
|
35
|
+
queryFn: () => {
|
|
36
|
+
if (entity.parent) {
|
|
37
|
+
return entityClient.list(parentId, listParams);
|
|
38
|
+
}
|
|
39
|
+
return entityClient.list(listParams);
|
|
40
|
+
},
|
|
28
41
|
enabled: entity.parent ? !!parentId : true,
|
|
29
|
-
...
|
|
42
|
+
...queryOptions
|
|
30
43
|
});
|
|
31
44
|
},
|
|
32
45
|
useGet(id, options) {
|
|
@@ -55,13 +68,35 @@ function createEntityHooks(entity, entityClient, keys, allQueryKeys) {
|
|
|
55
68
|
},
|
|
56
69
|
useUpdate(options) {
|
|
57
70
|
const queryClient = useQueryClient();
|
|
71
|
+
const isOptimistic = options?.optimistic ?? false;
|
|
58
72
|
return useMutation({
|
|
59
73
|
mutationFn: ({ id, dto }) => entityClient.update(id, dto),
|
|
74
|
+
onMutate: isOptimistic ? async ({ id, dto }) => {
|
|
75
|
+
await queryClient.cancelQueries({ queryKey: keys.all });
|
|
76
|
+
const previousData = queryClient.getQueriesData({ queryKey: keys.lists() });
|
|
77
|
+
queryClient.setQueriesData(
|
|
78
|
+
{ queryKey: keys.lists() },
|
|
79
|
+
(old) => old?.map(
|
|
80
|
+
(item) => isRecord(item) && item.id === id ? { ...item, ...dto } : item
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
return { previousData };
|
|
84
|
+
} : void 0,
|
|
85
|
+
onError: isOptimistic ? (_err, _vars, context) => {
|
|
86
|
+
const ctx = context;
|
|
87
|
+
if (ctx?.previousData) {
|
|
88
|
+
for (const [queryKey, data] of ctx.previousData) {
|
|
89
|
+
queryClient.setQueryData(queryKey, data);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} : void 0,
|
|
60
93
|
onSuccess: (...args) => {
|
|
61
|
-
queryClient.invalidateQueries({ queryKey: keys.all });
|
|
62
94
|
options?.onSuccess?.(...args);
|
|
63
95
|
},
|
|
64
|
-
|
|
96
|
+
onSettled: () => {
|
|
97
|
+
queryClient.invalidateQueries({ queryKey: keys.all });
|
|
98
|
+
},
|
|
99
|
+
...omit(options, ["onSuccess", "onMutate", "onError", "onSettled", "optimistic"])
|
|
65
100
|
});
|
|
66
101
|
},
|
|
67
102
|
useDelete(options) {
|
|
@@ -74,6 +109,37 @@ function createEntityHooks(entity, entityClient, keys, allQueryKeys) {
|
|
|
74
109
|
},
|
|
75
110
|
...omit(options, ["onSuccess"])
|
|
76
111
|
});
|
|
112
|
+
},
|
|
113
|
+
useInfiniteList(parentId, params, options) {
|
|
114
|
+
const limit = params?.limit ?? 20;
|
|
115
|
+
const keyParams = { infinite: true };
|
|
116
|
+
if (entity.parent && parentId) keyParams[entity.parent.param] = parentId;
|
|
117
|
+
if (params?.filters) keyParams.filters = params.filters;
|
|
118
|
+
if (params?.sort) keyParams.sort = params.sort;
|
|
119
|
+
return useInfiniteQuery({
|
|
120
|
+
queryKey: keys.list(keyParams),
|
|
121
|
+
queryFn: ({ pageParam }) => {
|
|
122
|
+
const pagination = typeof pageParam === "string" ? { type: "cursor", cursor: pageParam, limit } : { type: "offset", page: pageParam ?? 1, limit };
|
|
123
|
+
const listParams = {
|
|
124
|
+
filters: params?.filters,
|
|
125
|
+
sort: params?.sort,
|
|
126
|
+
pagination
|
|
127
|
+
};
|
|
128
|
+
if (entity.parent) {
|
|
129
|
+
return entityClient.list(parentId, listParams);
|
|
130
|
+
}
|
|
131
|
+
return entityClient.list(listParams);
|
|
132
|
+
},
|
|
133
|
+
initialPageParam: 1,
|
|
134
|
+
getNextPageParam: (lastPage) => {
|
|
135
|
+
const page = lastPage;
|
|
136
|
+
if (!page.meta?.hasNextPage) return void 0;
|
|
137
|
+
if (page.meta.nextCursor) return page.meta.nextCursor;
|
|
138
|
+
return void 0;
|
|
139
|
+
},
|
|
140
|
+
enabled: entity.parent ? !!parentId : true,
|
|
141
|
+
...options
|
|
142
|
+
});
|
|
77
143
|
}
|
|
78
144
|
};
|
|
79
145
|
}
|
|
@@ -105,5 +171,30 @@ function omit(obj, keys) {
|
|
|
105
171
|
}
|
|
106
172
|
return result;
|
|
107
173
|
}
|
|
174
|
+
function isRecord(value) {
|
|
175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
function isListParams(value) {
|
|
178
|
+
if (!isRecord(value)) return false;
|
|
179
|
+
return "filters" in value || "sort" in value || "pagination" in value;
|
|
180
|
+
}
|
|
181
|
+
function resolveListArgs(firstArg, secondArg, thirdArg) {
|
|
182
|
+
if (typeof firstArg === "string") {
|
|
183
|
+
return {
|
|
184
|
+
parentId: firstArg,
|
|
185
|
+
listParams: isListParams(secondArg) ? secondArg : void 0,
|
|
186
|
+
queryOptions: isListParams(secondArg) ? thirdArg : secondArg
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (isListParams(firstArg)) {
|
|
190
|
+
return {
|
|
191
|
+
listParams: firstArg,
|
|
192
|
+
queryOptions: secondArg
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
queryOptions: firstArg
|
|
197
|
+
};
|
|
198
|
+
}
|
|
108
199
|
|
|
109
200
|
export { deriveHooks };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplix-react/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "React Query hooks derived from @simplix-react/contract",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
"import": "./dist/index.js"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
|
-
"files": [
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
13
18
|
"scripts": {
|
|
14
19
|
"build": "tsup",
|
|
15
20
|
"dev": "tsup --watch",
|
|
@@ -25,6 +30,7 @@
|
|
|
25
30
|
"zod": ">=4.0.0"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
33
|
+
"@simplix-react/config-eslint": "workspace:*",
|
|
28
34
|
"@simplix-react/config-typescript": "workspace:*",
|
|
29
35
|
"@simplix-react/contract": "workspace:*",
|
|
30
36
|
"@tanstack/react-query": "^5.0.0",
|