@simplix-react/react 0.0.1 → 0.0.3

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 ADDED
@@ -0,0 +1,234 @@
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, simpleQueryBuilder } 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
+ queryBuilder: simpleQueryBuilder,
42
+ });
43
+
44
+ // 2. Derive hooks — one call generates everything
45
+ const hooks = deriveHooks(projectContract);
46
+
47
+ // 3. Use in components
48
+ function TaskList() {
49
+ const { data: tasks, isLoading } = hooks.task.useList();
50
+ const createTask = hooks.task.useCreate();
51
+
52
+ if (isLoading) return <p>Loading...</p>;
53
+
54
+ return (
55
+ <ul>
56
+ {tasks?.map((task) => (
57
+ <li key={task.id}>{task.title}</li>
58
+ ))}
59
+ </ul>
60
+ );
61
+ }
62
+ ```
63
+
64
+ ## API Overview
65
+
66
+ The package exports a single function and a set of type definitions:
67
+
68
+ | Export | Kind | Description |
69
+ | --- | --- | --- |
70
+ | `deriveHooks` | Function | Derives all hooks from a contract |
71
+ | `EntityHooks` | Type | Hook interface for a single entity |
72
+ | `OperationHooks` | Type | Hook interface for a custom operation |
73
+ | `DerivedListHook` | Type | List query hook signature |
74
+ | `DerivedGetHook` | Type | Detail query hook signature |
75
+ | `DerivedCreateHook` | Type | Create mutation hook signature |
76
+ | `DerivedUpdateHook` | Type | Update mutation hook signature |
77
+ | `DerivedDeleteHook` | Type | Delete mutation hook signature |
78
+ | `DerivedInfiniteListHook` | Type | Infinite list query hook signature |
79
+ | `OperationMutationHook` | Type | Operation mutation hook signature |
80
+
81
+ ## Key Concepts
82
+
83
+ ### Hook Derivation
84
+
85
+ `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.
86
+
87
+ ```ts
88
+ const hooks = deriveHooks(projectContract);
89
+ // hooks.task → EntityHooks<TaskSchema, CreateTaskSchema, UpdateTaskSchema>
90
+ // hooks.archiveProject → OperationHooks<ArchiveInput, ArchiveOutput>
91
+ ```
92
+
93
+ ### Auto-Invalidation
94
+
95
+ Mutation hooks automatically invalidate related queries:
96
+
97
+ - **Entity mutations** (`useCreate`, `useUpdate`, `useDelete`) invalidate all queries under the entity's query key scope.
98
+ - **Operation mutations** invalidate based on the `invalidates` function defined in the operation's contract configuration.
99
+
100
+ No manual `queryClient.invalidateQueries()` calls are needed.
101
+
102
+ ### TanStack Query Options Passthrough
103
+
104
+ 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`.
105
+
106
+ ```ts
107
+ // Pass query options
108
+ const { data } = hooks.task.useList({ enabled: false });
109
+
110
+ // Pass mutation options
111
+ const createTask = hooks.task.useCreate(undefined, {
112
+ onSuccess: (data) => console.log("Created:", data),
113
+ });
114
+ ```
115
+
116
+ ## Hook Reference
117
+
118
+ ### `useList`
119
+
120
+ Fetches a list of entities. Supports three calling conventions:
121
+
122
+ ```ts
123
+ // Top-level entity
124
+ hooks.task.useList();
125
+ hooks.task.useList({ enabled: isReady });
126
+
127
+ // With filters/sort
128
+ hooks.task.useList({
129
+ filters: { status: "open" },
130
+ sort: { field: "createdAt", direction: "desc" },
131
+ });
132
+
133
+ // Child entity with parent ID
134
+ hooks.task.useList(projectId);
135
+ hooks.task.useList(projectId, { filters: { status: "open" } });
136
+ hooks.task.useList(projectId, { filters: { status: "open" } }, { enabled: isReady });
137
+ ```
138
+
139
+ For child entities, the query is automatically disabled when `parentId` is falsy.
140
+
141
+ ### `useGet`
142
+
143
+ Fetches a single entity by ID.
144
+
145
+ ```ts
146
+ const { data: task } = hooks.task.useGet(taskId);
147
+ const { data: task } = hooks.task.useGet(taskId, { staleTime: 5000 });
148
+ ```
149
+
150
+ The query is automatically disabled when `id` is falsy.
151
+
152
+ ### `useCreate`
153
+
154
+ Creates a new entity. For child entities, pass the parent ID.
155
+
156
+ ```ts
157
+ // Top-level entity
158
+ const createTask = hooks.task.useCreate();
159
+ createTask.mutate({ title: "New task", status: "open" });
160
+
161
+ // Child entity
162
+ const createTask = hooks.task.useCreate(projectId);
163
+ createTask.mutate({ title: "New task", status: "open" });
164
+ ```
165
+
166
+ ### `useUpdate`
167
+
168
+ Updates an existing entity. Supports optimistic updates.
169
+
170
+ ```ts
171
+ // Standard update
172
+ const updateTask = hooks.task.useUpdate();
173
+ updateTask.mutate({ id: taskId, dto: { status: "done" } });
174
+
175
+ // Optimistic update — UI updates instantly, rolls back on error
176
+ const updateTask = hooks.task.useUpdate({ optimistic: true });
177
+ updateTask.mutate({ id: taskId, dto: { status: "done" } });
178
+ ```
179
+
180
+ ### `useDelete`
181
+
182
+ Deletes an entity by ID.
183
+
184
+ ```ts
185
+ const deleteTask = hooks.task.useDelete();
186
+ deleteTask.mutate(taskId);
187
+ ```
188
+
189
+ ### `useInfiniteList`
190
+
191
+ Fetches paginated data with cursor-based or offset-based pagination. Pagination is managed automatically based on the response's `meta` field.
192
+
193
+ ```ts
194
+ const {
195
+ data,
196
+ fetchNextPage,
197
+ hasNextPage,
198
+ isFetchingNextPage,
199
+ } = hooks.task.useInfiniteList(projectId, {
200
+ limit: 10,
201
+ filters: { status: "open" },
202
+ sort: { field: "createdAt", direction: "desc" },
203
+ });
204
+
205
+ // Access flattened data
206
+ const allTasks = data?.pages.flatMap((page) => page.data) ?? [];
207
+ ```
208
+
209
+ ### Operation `useMutation`
210
+
211
+ Custom operations defined in the contract each produce a `useMutation` hook.
212
+
213
+ ```ts
214
+ const archiveProject = hooks.archiveProject.useMutation({
215
+ onSuccess: () => {
216
+ // Cache invalidation is already handled via the contract's `invalidates`
217
+ console.log("Project archived");
218
+ },
219
+ });
220
+
221
+ archiveProject.mutate({ projectId: "abc" });
222
+ ```
223
+
224
+ ## Related Packages
225
+
226
+ | Package | Description |
227
+ | --- | --- |
228
+ | `@simplix-react/contract` | Define type-safe API contracts |
229
+ | `@simplix-react/mock` | Generate MSW handlers from contracts for testing |
230
+ | `@simplix-react/i18n` | i18next-based internationalization framework |
231
+
232
+ ---
233
+
234
+ 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, AnyEntityDef, AnyOperationDef, ApiContractConfig, QueryKeyFactory, OperationDefinition } from '@simplix-react/contract';
2
+ import { UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult, UseInfiniteQueryResult } from '@tanstack/react-query';
2
3
  import { z } from 'zod';
3
- import { UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
4
4
 
5
5
  /**
6
- * Derived query hook with options passthrough (C8 fix)
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
- * Entity hooks derived from EntityDefinition
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,25 +174,107 @@ 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
- * Operation mutation hook
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
  }
36
225
 
37
- type AnyEntityDef = EntityDefinition<z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny>;
38
- type AnyOperationDef = OperationDefinition<z.ZodTypeAny, z.ZodTypeAny>;
39
226
  /**
40
- * Derive React Query hooks from an API contract.
227
+ * Derives type-safe React Query hooks from an API contract.
228
+ *
229
+ * Generates a complete set of hooks for every entity and operation defined in
230
+ * the contract. Entity hooks include `useList`, `useGet`, `useCreate`,
231
+ * `useUpdate`, `useDelete`, and `useInfiniteList`. Operation hooks provide
232
+ * a single `useMutation` with automatic cache invalidation.
233
+ *
234
+ * All hooks support full TanStack Query options passthrough — callers can
235
+ * provide any option except `queryKey`/`queryFn` (for queries) or
236
+ * `mutationFn` (for mutations), which are managed internally.
237
+ *
238
+ * @typeParam TEntities - Record of entity definitions from the contract
239
+ * @typeParam TOperations - Record of operation definitions from the contract
240
+ *
241
+ * @param contract - The API contract produced by `defineApi()` from `@simplix-react/contract`,
242
+ * containing `config`, `client`, and `queryKeys`.
243
+ *
244
+ * @returns An object keyed by entity/operation name, each containing its derived hooks.
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * import { defineApi } from "@simplix-react/contract";
249
+ * import { deriveHooks } from "@simplix-react/react";
250
+ * import { z } from "zod";
251
+ *
252
+ * const projectContract = defineApi({
253
+ * domain: "project",
254
+ * basePath: "/api",
255
+ * entities: {
256
+ * task: {
257
+ * path: "/tasks",
258
+ * schema: z.object({ id: z.string(), title: z.string(), status: z.string() }),
259
+ * createSchema: z.object({ title: z.string(), status: z.string() }),
260
+ * updateSchema: z.object({ title: z.string().optional(), status: z.string().optional() }),
261
+ * },
262
+ * },
263
+ * });
264
+ *
265
+ * // Derive all hooks at once
266
+ * const hooks = deriveHooks(projectContract);
41
267
  *
42
- * For each entity, generates: useList, useGet, useCreate, useUpdate, useDelete
43
- * For each operation, generates: useMutation
268
+ * // Use in components
269
+ * function TaskList() {
270
+ * const { data: tasks } = hooks.task.useList();
271
+ * const createTask = hooks.task.useCreate();
272
+ * // ...
273
+ * }
274
+ * ```
44
275
  *
45
- * All hooks support options passthrough (C8 fix) and
46
- * cache invalidation via operations.invalidates (C7 fix).
276
+ * @see {@link EntityHooks} for the per-entity hook interface.
277
+ * @see {@link OperationHooks} for the per-operation hook interface.
47
278
  */
48
279
  declare function deriveHooks<TEntities extends Record<string, AnyEntityDef>, TOperations extends Record<string, AnyOperationDef>>(contract: {
49
280
  config: ApiContractConfig<TEntities, TOperations>;
@@ -56,4 +287,4 @@ type DerivedHooksResult<TEntities extends Record<string, AnyEntityDef>, TOperati
56
287
  [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput> ? OperationHooks<TInput, TOutput> : never;
57
288
  };
58
289
 
59
- export { type DerivedCreateHook, type DerivedDeleteHook, type DerivedGetHook, type DerivedListHook, type DerivedUpdateHook, type EntityHooks, type OperationHooks, type OperationMutationHook, deriveHooks };
290
+ 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, allQueryKeys) {
22
+ function createEntityHooks(entity, entityClient, keys, _allQueryKeys, _config) {
23
23
  return {
24
- useList(parentId, options) {
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(entity.parent ? { [entity.parent.param]: parentId } : {}),
27
- queryFn: () => entityClient.list(entity.parent ? parentId : void 0),
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
- ...options
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
- ...omit(options, ["onSuccess"])
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, _allPages, lastPageParam) => {
135
+ const page = lastPage;
136
+ if (!page.meta?.hasNextPage) return void 0;
137
+ if (page.meta.nextCursor) return page.meta.nextCursor;
138
+ return typeof lastPageParam === "number" ? lastPageParam + 1 : 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,39 +1,49 @@
1
1
  {
2
2
  "name": "@simplix-react/react",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "React Query hooks derived from @simplix-react/contract",
5
5
  "type": "module",
6
+ "sideEffects": false,
6
7
  "exports": {
7
8
  ".": {
8
9
  "types": "./dist/index.d.ts",
9
10
  "import": "./dist/index.js"
10
11
  }
11
12
  },
12
- "files": ["dist"],
13
- "scripts": {
14
- "build": "tsup",
15
- "dev": "tsup --watch",
16
- "typecheck": "tsc --noEmit",
17
- "lint": "eslint src",
18
- "test": "vitest run --passWithNoTests",
19
- "clean": "rm -rf dist .turbo"
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
20
18
  },
21
19
  "peerDependencies": {
22
- "@simplix-react/contract": "workspace:*",
23
20
  "@tanstack/react-query": ">=5.0.0",
24
21
  "react": ">=18.0.0",
25
- "zod": ">=4.0.0"
22
+ "zod": ">=4.0.0",
23
+ "@simplix-react/contract": "0.0.3"
26
24
  },
27
25
  "devDependencies": {
28
- "@simplix-react/config-typescript": "workspace:*",
29
- "@simplix-react/contract": "workspace:*",
30
26
  "@tanstack/react-query": "^5.0.0",
27
+ "@testing-library/jest-dom": "^6.9.1",
28
+ "@testing-library/react": "^16.3.2",
31
29
  "@types/react": "^19.0.0",
32
30
  "eslint": "^9.39.2",
31
+ "jsdom": "^28.0.0",
33
32
  "react": "^19.0.0",
34
33
  "tsup": "^8.5.1",
35
34
  "typescript": "^5.9.3",
36
35
  "vitest": "^3.0.0",
37
- "zod": "^4.0.0"
36
+ "zod": "^4.0.0",
37
+ "@simplix-react/contract": "0.0.3",
38
+ "@simplix-react/config-eslint": "0.0.1",
39
+ "@simplix-react/config-typescript": "0.0.1"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "typecheck": "tsc --noEmit",
45
+ "lint": "eslint src",
46
+ "test": "vitest run --passWithNoTests",
47
+ "clean": "rm -rf dist .turbo"
38
48
  }
39
- }
49
+ }