enlace 0.0.1-beta.2 → 0.0.1-beta.4
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 +143 -15
- package/dist/index.d.mts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +73 -14
- package/dist/index.mjs +73 -14
- package/dist/next/hook/index.d.mts +18 -1
- package/dist/next/hook/index.d.ts +18 -1
- package/dist/next/hook/index.js +75 -14
- package/dist/next/hook/index.mjs +75 -14
- package/dist/next/index.d.mts +8 -0
- package/dist/next/index.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ import { createEnlaceHook, Endpoint } from "enlace";
|
|
|
15
15
|
|
|
16
16
|
type ApiSchema = {
|
|
17
17
|
posts: {
|
|
18
|
-
$get: Endpoint<Post[]>;
|
|
18
|
+
$get: Endpoint<Post[], ApiError>;
|
|
19
19
|
$post: Endpoint<Post, ApiError, CreatePost>;
|
|
20
20
|
_: {
|
|
21
|
-
$get: Endpoint<Post>;
|
|
22
|
-
$delete: Endpoint<void>;
|
|
21
|
+
$get: Endpoint<Post, ApiError>;
|
|
22
|
+
$delete: Endpoint<void, ApiError>;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
};
|
|
@@ -52,14 +52,14 @@ import { Endpoint } from "enlace";
|
|
|
52
52
|
|
|
53
53
|
type ApiSchema = {
|
|
54
54
|
users: {
|
|
55
|
-
$get: Endpoint<User[]>; // GET /users
|
|
56
|
-
$post: Endpoint<User>; // POST /users
|
|
57
|
-
_: {
|
|
58
|
-
$get: Endpoint<User>; // GET /users/:id
|
|
59
|
-
$put: Endpoint<User>; // PUT /users/:id
|
|
60
|
-
$delete: Endpoint<void>; // DELETE /users/:id
|
|
55
|
+
$get: Endpoint<User[], ApiError>; // GET /users
|
|
56
|
+
$post: Endpoint<User, ApiError>; // POST /users
|
|
57
|
+
_: { // /users/:id
|
|
58
|
+
$get: Endpoint<User, ApiError>; // GET /users/:id
|
|
59
|
+
$put: Endpoint<User, ApiError>; // PUT /users/:id
|
|
60
|
+
$delete: Endpoint<void, ApiError>; // DELETE /users/:id
|
|
61
61
|
profile: {
|
|
62
|
-
$get: Endpoint<Profile>; // GET /users/:id/profile
|
|
62
|
+
$get: Endpoint<Profile, ApiError>; // GET /users/:id/profile
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
65
|
};
|
|
@@ -74,16 +74,16 @@ api.users[123].profile.get(); // GET /users/123/profile
|
|
|
74
74
|
### Endpoint Type
|
|
75
75
|
|
|
76
76
|
```typescript
|
|
77
|
-
type Endpoint<TData, TError
|
|
77
|
+
type Endpoint<TData, TError, TBody = never> = {
|
|
78
78
|
data: TData; // Response data type
|
|
79
|
-
error: TError; // Error response type
|
|
80
|
-
body: TBody; // Request body type
|
|
79
|
+
error: TError; // Error response type (required)
|
|
80
|
+
body: TBody; // Request body type (optional)
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
// Examples
|
|
84
|
-
type GetUsers = Endpoint<User[]>;
|
|
84
|
+
type GetUsers = Endpoint<User[], ApiError>; // GET, no body
|
|
85
85
|
type CreateUser = Endpoint<User, ApiError, CreateUserInput>; // POST with body
|
|
86
|
-
type DeleteUser = Endpoint<void, NotFoundError>;
|
|
86
|
+
type DeleteUser = Endpoint<void, NotFoundError>; // DELETE, no response data
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
## React Hooks
|
|
@@ -115,6 +115,35 @@ function Posts({ page, limit }: { page: number; limit: number }) {
|
|
|
115
115
|
- Auto-fetches on mount
|
|
116
116
|
- Re-fetches when dependencies change (no deps array needed!)
|
|
117
117
|
- Returns cached data while revalidating
|
|
118
|
+
- **Request deduplication** — identical requests from multiple components trigger only one fetch
|
|
119
|
+
|
|
120
|
+
### Conditional Fetching
|
|
121
|
+
|
|
122
|
+
Skip fetching with the `enabled` option:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
function ProductForm({ id }: { id: string | "new" }) {
|
|
126
|
+
// Skip fetching when creating a new product
|
|
127
|
+
const { data, loading } = useAPI(
|
|
128
|
+
(api) => api.products[id].get(),
|
|
129
|
+
{ enabled: id !== "new" }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (id === "new") return <CreateProductForm />;
|
|
133
|
+
if (loading) return <div>Loading...</div>;
|
|
134
|
+
return <EditProductForm product={data} />;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Also useful when waiting for a dependency
|
|
140
|
+
function UserPosts({ userId }: { userId: string | undefined }) {
|
|
141
|
+
const { data } = useAPI(
|
|
142
|
+
(api) => api.users[userId!].posts.get(),
|
|
143
|
+
{ enabled: userId !== undefined }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
118
147
|
|
|
119
148
|
```typescript
|
|
120
149
|
function Post({ id }: { id: number }) {
|
|
@@ -124,6 +153,34 @@ function Post({ id }: { id: number }) {
|
|
|
124
153
|
}
|
|
125
154
|
```
|
|
126
155
|
|
|
156
|
+
### Request Deduplication
|
|
157
|
+
|
|
158
|
+
Multiple components requesting the same data will share a single network request:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Both components render at the same time
|
|
162
|
+
function PostTitle({ id }: { id: number }) {
|
|
163
|
+
const { data } = useAPI((api) => api.posts[id].get());
|
|
164
|
+
return <h1>{data?.title}</h1>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function PostBody({ id }: { id: number }) {
|
|
168
|
+
const { data } = useAPI((api) => api.posts[id].get());
|
|
169
|
+
return <p>{data?.body}</p>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Only ONE fetch request is made to GET /posts/123
|
|
173
|
+
// Both components share the same cached result
|
|
174
|
+
function PostPage() {
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<PostTitle id={123} />
|
|
178
|
+
<PostBody id={123} />
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
127
184
|
### Selector Mode (Manual Trigger)
|
|
128
185
|
|
|
129
186
|
For mutations or lazy-loaded requests:
|
|
@@ -157,6 +214,56 @@ function CreatePost() {
|
|
|
157
214
|
}
|
|
158
215
|
```
|
|
159
216
|
|
|
217
|
+
### Dynamic Path Parameters
|
|
218
|
+
|
|
219
|
+
Use `:paramName` syntax for dynamic IDs passed at trigger time:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
function PostList({ posts }: { posts: Post[] }) {
|
|
223
|
+
// Define once with :id placeholder
|
|
224
|
+
const { trigger, loading } = useAPI((api) => api.posts[":id"].delete);
|
|
225
|
+
|
|
226
|
+
const handleDelete = (postId: number) => {
|
|
227
|
+
// Pass the actual ID when triggering
|
|
228
|
+
trigger({ pathParams: { id: postId } });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<ul>
|
|
233
|
+
{posts.map((post) => (
|
|
234
|
+
<li key={post.id}>
|
|
235
|
+
{post.title}
|
|
236
|
+
<button onClick={() => handleDelete(post.id)} disabled={loading}>
|
|
237
|
+
Delete
|
|
238
|
+
</button>
|
|
239
|
+
</li>
|
|
240
|
+
))}
|
|
241
|
+
</ul>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Multiple path parameters:**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const { trigger } = useAPI((api) => api.users[":userId"].posts[":postId"].delete);
|
|
250
|
+
|
|
251
|
+
trigger({ pathParams: { userId: "1", postId: "42" } });
|
|
252
|
+
// → DELETE /users/1/posts/42
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**With request body:**
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const { trigger } = useAPI((api) => api.products[":id"].patch);
|
|
259
|
+
|
|
260
|
+
trigger({
|
|
261
|
+
pathParams: { id: "123" },
|
|
262
|
+
body: { name: "Updated Product" },
|
|
263
|
+
});
|
|
264
|
+
// → PATCH /products/123 with body
|
|
265
|
+
```
|
|
266
|
+
|
|
160
267
|
## Caching & Auto-Revalidation
|
|
161
268
|
|
|
162
269
|
### Automatic Cache Tags (Zero Config)
|
|
@@ -259,6 +366,15 @@ const useAPI = createEnlaceHook<ApiSchema>(
|
|
|
259
366
|
### Query Mode
|
|
260
367
|
|
|
261
368
|
```typescript
|
|
369
|
+
// Basic usage
|
|
370
|
+
const result = useAPI((api) => api.posts.get());
|
|
371
|
+
|
|
372
|
+
// With options
|
|
373
|
+
const result = useAPI(
|
|
374
|
+
(api) => api.posts.get(),
|
|
375
|
+
{ enabled: true } // Skip fetching when false
|
|
376
|
+
);
|
|
377
|
+
|
|
262
378
|
type UseEnlaceQueryResult<TData, TError> = {
|
|
263
379
|
loading: boolean; // No cached data and fetching
|
|
264
380
|
fetching: boolean; // Request in progress
|
|
@@ -281,6 +397,18 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
281
397
|
};
|
|
282
398
|
```
|
|
283
399
|
|
|
400
|
+
### Request Options
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
type RequestOptions = {
|
|
404
|
+
query?: Record<string, unknown>; // Query parameters
|
|
405
|
+
body?: TBody; // Request body
|
|
406
|
+
tags?: string[]; // Cache tags (GET only)
|
|
407
|
+
revalidateTags?: string[]; // Tags to invalidate after mutation
|
|
408
|
+
pathParams?: Record<string, string | number>; // Dynamic path parameters
|
|
409
|
+
};
|
|
410
|
+
```
|
|
411
|
+
|
|
284
412
|
---
|
|
285
413
|
|
|
286
414
|
## Next.js Integration
|
package/dist/index.d.mts
CHANGED
|
@@ -11,6 +11,23 @@ type ReactRequestOptionsBase = {
|
|
|
11
11
|
tags?: string[];
|
|
12
12
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
13
13
|
revalidateTags?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Path parameters for dynamic URL segments.
|
|
16
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
17
|
+
* @example
|
|
18
|
+
* // With path api.products[':id'].delete
|
|
19
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
20
|
+
*/
|
|
21
|
+
pathParams?: Record<string, string | number>;
|
|
22
|
+
};
|
|
23
|
+
/** Options for query mode hooks */
|
|
24
|
+
type UseEnlaceQueryOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Whether the query should execute.
|
|
27
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
14
31
|
};
|
|
15
32
|
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
16
33
|
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -70,7 +87,7 @@ type EnlaceHookOptions = {
|
|
|
70
87
|
};
|
|
71
88
|
type EnlaceHook<TSchema> = {
|
|
72
89
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
73
|
-
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError
|
|
90
|
+
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
74
91
|
};
|
|
75
92
|
/**
|
|
76
93
|
* Creates a React hook for making API calls.
|
|
@@ -94,4 +111,4 @@ declare function onRevalidate(callback: Listener): () => void;
|
|
|
94
111
|
|
|
95
112
|
declare function clearCache(key?: string): void;
|
|
96
113
|
|
|
97
|
-
export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
|
|
114
|
+
export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,23 @@ type ReactRequestOptionsBase = {
|
|
|
11
11
|
tags?: string[];
|
|
12
12
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
13
13
|
revalidateTags?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Path parameters for dynamic URL segments.
|
|
16
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
17
|
+
* @example
|
|
18
|
+
* // With path api.products[':id'].delete
|
|
19
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
20
|
+
*/
|
|
21
|
+
pathParams?: Record<string, string | number>;
|
|
22
|
+
};
|
|
23
|
+
/** Options for query mode hooks */
|
|
24
|
+
type UseEnlaceQueryOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Whether the query should execute.
|
|
27
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
14
31
|
};
|
|
15
32
|
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
16
33
|
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -70,7 +87,7 @@ type EnlaceHookOptions = {
|
|
|
70
87
|
};
|
|
71
88
|
type EnlaceHook<TSchema> = {
|
|
72
89
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
73
|
-
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError
|
|
90
|
+
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
74
91
|
};
|
|
75
92
|
/**
|
|
76
93
|
* Creates a React hook for making API calls.
|
|
@@ -94,4 +111,4 @@ declare function onRevalidate(callback: Listener): () => void;
|
|
|
94
111
|
|
|
95
112
|
declare function clearCache(key?: string): void;
|
|
96
113
|
|
|
97
|
-
export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
|
|
114
|
+
export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
|
package/dist/index.js
CHANGED
|
@@ -184,11 +184,26 @@ function onRevalidate(callback) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// src/react/useQueryMode.ts
|
|
187
|
+
function resolvePath(path, pathParams) {
|
|
188
|
+
if (!pathParams) return path;
|
|
189
|
+
return path.map((segment) => {
|
|
190
|
+
if (segment.startsWith(":")) {
|
|
191
|
+
const paramName = segment.slice(1);
|
|
192
|
+
const value = pathParams[paramName];
|
|
193
|
+
if (value === void 0) {
|
|
194
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
195
|
+
}
|
|
196
|
+
return String(value);
|
|
197
|
+
}
|
|
198
|
+
return segment;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
187
201
|
function useQueryMode(api, trackedCall, options) {
|
|
188
|
-
const { autoGenerateTags, staleTime } = options;
|
|
202
|
+
const { autoGenerateTags, staleTime, enabled } = options;
|
|
189
203
|
const queryKey = createQueryKey(trackedCall);
|
|
190
204
|
const requestOptions = trackedCall.options;
|
|
191
|
-
const
|
|
205
|
+
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
|
|
206
|
+
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
192
207
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
193
208
|
const cached = getCache(queryKey);
|
|
194
209
|
const hasCachedData = cached?.data !== void 0;
|
|
@@ -207,6 +222,15 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
207
222
|
const fetchRef = (0, import_react.useRef)(null);
|
|
208
223
|
(0, import_react.useEffect)(() => {
|
|
209
224
|
mountedRef.current = true;
|
|
225
|
+
if (!enabled) {
|
|
226
|
+
dispatch({
|
|
227
|
+
type: "RESET",
|
|
228
|
+
state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
|
|
229
|
+
});
|
|
230
|
+
return () => {
|
|
231
|
+
mountedRef.current = false;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
210
234
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
211
235
|
const unsubscribe = subscribeCache(queryKey, () => {
|
|
212
236
|
if (mountedRef.current) {
|
|
@@ -220,7 +244,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
220
244
|
}
|
|
221
245
|
dispatch({ type: "FETCH_START" });
|
|
222
246
|
let current = api;
|
|
223
|
-
for (const segment of
|
|
247
|
+
for (const segment of resolvedPath) {
|
|
224
248
|
current = current[segment];
|
|
225
249
|
}
|
|
226
250
|
const method = current[trackedCall.method];
|
|
@@ -252,7 +276,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
252
276
|
fetchRef.current = null;
|
|
253
277
|
unsubscribe();
|
|
254
278
|
};
|
|
255
|
-
}, [queryKey]);
|
|
279
|
+
}, [queryKey, enabled]);
|
|
256
280
|
(0, import_react.useEffect)(() => {
|
|
257
281
|
if (queryTags.length === 0) return;
|
|
258
282
|
return onRevalidate((invalidatedTags) => {
|
|
@@ -299,23 +323,56 @@ function createTrackingProxy(onTrack) {
|
|
|
299
323
|
|
|
300
324
|
// src/react/useSelectorMode.ts
|
|
301
325
|
var import_react2 = require("react");
|
|
302
|
-
function
|
|
326
|
+
function resolvePath2(path, pathParams) {
|
|
327
|
+
if (!pathParams) return path;
|
|
328
|
+
return path.map((segment) => {
|
|
329
|
+
if (segment.startsWith(":")) {
|
|
330
|
+
const paramName = segment.slice(1);
|
|
331
|
+
const value = pathParams[paramName];
|
|
332
|
+
if (value === void 0) {
|
|
333
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
334
|
+
}
|
|
335
|
+
return String(value);
|
|
336
|
+
}
|
|
337
|
+
return segment;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function hasPathParams(path) {
|
|
341
|
+
return path.some((segment) => segment.startsWith(":"));
|
|
342
|
+
}
|
|
343
|
+
function useSelectorMode(config) {
|
|
344
|
+
const { method, api, path, methodName, autoRevalidateTags } = config;
|
|
303
345
|
const [state, dispatch] = (0, import_react2.useReducer)(hookReducer, initialState);
|
|
304
346
|
const methodRef = (0, import_react2.useRef)(method);
|
|
347
|
+
const apiRef = (0, import_react2.useRef)(api);
|
|
305
348
|
const triggerRef = (0, import_react2.useRef)(null);
|
|
306
349
|
const pathRef = (0, import_react2.useRef)(path);
|
|
350
|
+
const methodNameRef = (0, import_react2.useRef)(methodName);
|
|
307
351
|
const autoRevalidateRef = (0, import_react2.useRef)(autoRevalidateTags);
|
|
308
352
|
methodRef.current = method;
|
|
353
|
+
apiRef.current = api;
|
|
309
354
|
pathRef.current = path;
|
|
355
|
+
methodNameRef.current = methodName;
|
|
310
356
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
311
357
|
if (!triggerRef.current) {
|
|
312
358
|
triggerRef.current = (async (...args) => {
|
|
313
359
|
dispatch({ type: "FETCH_START" });
|
|
314
|
-
const
|
|
360
|
+
const options = args[0];
|
|
361
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
|
|
362
|
+
let res;
|
|
363
|
+
if (hasPathParams(pathRef.current)) {
|
|
364
|
+
let current = apiRef.current;
|
|
365
|
+
for (const segment of resolvedPath) {
|
|
366
|
+
current = current[segment];
|
|
367
|
+
}
|
|
368
|
+
const resolvedMethod = current[methodNameRef.current];
|
|
369
|
+
res = await resolvedMethod(...args);
|
|
370
|
+
} else {
|
|
371
|
+
res = await methodRef.current(...args);
|
|
372
|
+
}
|
|
315
373
|
if (res.ok) {
|
|
316
374
|
dispatch({ type: "FETCH_SUCCESS", data: res.data });
|
|
317
|
-
const
|
|
318
|
-
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
|
|
375
|
+
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
|
|
319
376
|
if (tagsToInvalidate.length > 0) {
|
|
320
377
|
invalidateTags(tagsToInvalidate);
|
|
321
378
|
}
|
|
@@ -339,7 +396,7 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
339
396
|
autoRevalidateTags = true,
|
|
340
397
|
staleTime = 0
|
|
341
398
|
} = hookOptions;
|
|
342
|
-
function useEnlaceHook(selectorOrQuery) {
|
|
399
|
+
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
343
400
|
let trackingResult = {
|
|
344
401
|
trackedCall: null,
|
|
345
402
|
selectorPath: null,
|
|
@@ -353,16 +410,18 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
353
410
|
);
|
|
354
411
|
if (typeof result === "function") {
|
|
355
412
|
const actualResult = selectorOrQuery(api);
|
|
356
|
-
return useSelectorMode(
|
|
357
|
-
actualResult,
|
|
358
|
-
|
|
413
|
+
return useSelectorMode({
|
|
414
|
+
method: actualResult,
|
|
415
|
+
api,
|
|
416
|
+
path: trackingResult.selectorPath ?? [],
|
|
417
|
+
methodName: trackingResult.selectorMethod ?? "",
|
|
359
418
|
autoRevalidateTags
|
|
360
|
-
);
|
|
419
|
+
});
|
|
361
420
|
}
|
|
362
421
|
return useQueryMode(
|
|
363
422
|
api,
|
|
364
423
|
trackingResult.trackedCall,
|
|
365
|
-
{ autoGenerateTags, staleTime }
|
|
424
|
+
{ autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
|
|
366
425
|
);
|
|
367
426
|
}
|
|
368
427
|
return useEnlaceHook;
|
package/dist/index.mjs
CHANGED
|
@@ -157,11 +157,26 @@ function onRevalidate(callback) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// src/react/useQueryMode.ts
|
|
160
|
+
function resolvePath(path, pathParams) {
|
|
161
|
+
if (!pathParams) return path;
|
|
162
|
+
return path.map((segment) => {
|
|
163
|
+
if (segment.startsWith(":")) {
|
|
164
|
+
const paramName = segment.slice(1);
|
|
165
|
+
const value = pathParams[paramName];
|
|
166
|
+
if (value === void 0) {
|
|
167
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
168
|
+
}
|
|
169
|
+
return String(value);
|
|
170
|
+
}
|
|
171
|
+
return segment;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
160
174
|
function useQueryMode(api, trackedCall, options) {
|
|
161
|
-
const { autoGenerateTags, staleTime } = options;
|
|
175
|
+
const { autoGenerateTags, staleTime, enabled } = options;
|
|
162
176
|
const queryKey = createQueryKey(trackedCall);
|
|
163
177
|
const requestOptions = trackedCall.options;
|
|
164
|
-
const
|
|
178
|
+
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
|
|
179
|
+
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
165
180
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
166
181
|
const cached = getCache(queryKey);
|
|
167
182
|
const hasCachedData = cached?.data !== void 0;
|
|
@@ -180,6 +195,15 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
180
195
|
const fetchRef = useRef(null);
|
|
181
196
|
useEffect(() => {
|
|
182
197
|
mountedRef.current = true;
|
|
198
|
+
if (!enabled) {
|
|
199
|
+
dispatch({
|
|
200
|
+
type: "RESET",
|
|
201
|
+
state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
|
|
202
|
+
});
|
|
203
|
+
return () => {
|
|
204
|
+
mountedRef.current = false;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
183
207
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
184
208
|
const unsubscribe = subscribeCache(queryKey, () => {
|
|
185
209
|
if (mountedRef.current) {
|
|
@@ -193,7 +217,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
193
217
|
}
|
|
194
218
|
dispatch({ type: "FETCH_START" });
|
|
195
219
|
let current = api;
|
|
196
|
-
for (const segment of
|
|
220
|
+
for (const segment of resolvedPath) {
|
|
197
221
|
current = current[segment];
|
|
198
222
|
}
|
|
199
223
|
const method = current[trackedCall.method];
|
|
@@ -225,7 +249,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
225
249
|
fetchRef.current = null;
|
|
226
250
|
unsubscribe();
|
|
227
251
|
};
|
|
228
|
-
}, [queryKey]);
|
|
252
|
+
}, [queryKey, enabled]);
|
|
229
253
|
useEffect(() => {
|
|
230
254
|
if (queryTags.length === 0) return;
|
|
231
255
|
return onRevalidate((invalidatedTags) => {
|
|
@@ -272,23 +296,56 @@ function createTrackingProxy(onTrack) {
|
|
|
272
296
|
|
|
273
297
|
// src/react/useSelectorMode.ts
|
|
274
298
|
import { useRef as useRef2, useReducer as useReducer2 } from "react";
|
|
275
|
-
function
|
|
299
|
+
function resolvePath2(path, pathParams) {
|
|
300
|
+
if (!pathParams) return path;
|
|
301
|
+
return path.map((segment) => {
|
|
302
|
+
if (segment.startsWith(":")) {
|
|
303
|
+
const paramName = segment.slice(1);
|
|
304
|
+
const value = pathParams[paramName];
|
|
305
|
+
if (value === void 0) {
|
|
306
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
307
|
+
}
|
|
308
|
+
return String(value);
|
|
309
|
+
}
|
|
310
|
+
return segment;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function hasPathParams(path) {
|
|
314
|
+
return path.some((segment) => segment.startsWith(":"));
|
|
315
|
+
}
|
|
316
|
+
function useSelectorMode(config) {
|
|
317
|
+
const { method, api, path, methodName, autoRevalidateTags } = config;
|
|
276
318
|
const [state, dispatch] = useReducer2(hookReducer, initialState);
|
|
277
319
|
const methodRef = useRef2(method);
|
|
320
|
+
const apiRef = useRef2(api);
|
|
278
321
|
const triggerRef = useRef2(null);
|
|
279
322
|
const pathRef = useRef2(path);
|
|
323
|
+
const methodNameRef = useRef2(methodName);
|
|
280
324
|
const autoRevalidateRef = useRef2(autoRevalidateTags);
|
|
281
325
|
methodRef.current = method;
|
|
326
|
+
apiRef.current = api;
|
|
282
327
|
pathRef.current = path;
|
|
328
|
+
methodNameRef.current = methodName;
|
|
283
329
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
284
330
|
if (!triggerRef.current) {
|
|
285
331
|
triggerRef.current = (async (...args) => {
|
|
286
332
|
dispatch({ type: "FETCH_START" });
|
|
287
|
-
const
|
|
333
|
+
const options = args[0];
|
|
334
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
|
|
335
|
+
let res;
|
|
336
|
+
if (hasPathParams(pathRef.current)) {
|
|
337
|
+
let current = apiRef.current;
|
|
338
|
+
for (const segment of resolvedPath) {
|
|
339
|
+
current = current[segment];
|
|
340
|
+
}
|
|
341
|
+
const resolvedMethod = current[methodNameRef.current];
|
|
342
|
+
res = await resolvedMethod(...args);
|
|
343
|
+
} else {
|
|
344
|
+
res = await methodRef.current(...args);
|
|
345
|
+
}
|
|
288
346
|
if (res.ok) {
|
|
289
347
|
dispatch({ type: "FETCH_SUCCESS", data: res.data });
|
|
290
|
-
const
|
|
291
|
-
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
|
|
348
|
+
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
|
|
292
349
|
if (tagsToInvalidate.length > 0) {
|
|
293
350
|
invalidateTags(tagsToInvalidate);
|
|
294
351
|
}
|
|
@@ -312,7 +369,7 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
312
369
|
autoRevalidateTags = true,
|
|
313
370
|
staleTime = 0
|
|
314
371
|
} = hookOptions;
|
|
315
|
-
function useEnlaceHook(selectorOrQuery) {
|
|
372
|
+
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
316
373
|
let trackingResult = {
|
|
317
374
|
trackedCall: null,
|
|
318
375
|
selectorPath: null,
|
|
@@ -326,16 +383,18 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
326
383
|
);
|
|
327
384
|
if (typeof result === "function") {
|
|
328
385
|
const actualResult = selectorOrQuery(api);
|
|
329
|
-
return useSelectorMode(
|
|
330
|
-
actualResult,
|
|
331
|
-
|
|
386
|
+
return useSelectorMode({
|
|
387
|
+
method: actualResult,
|
|
388
|
+
api,
|
|
389
|
+
path: trackingResult.selectorPath ?? [],
|
|
390
|
+
methodName: trackingResult.selectorMethod ?? "",
|
|
332
391
|
autoRevalidateTags
|
|
333
|
-
);
|
|
392
|
+
});
|
|
334
393
|
}
|
|
335
394
|
return useQueryMode(
|
|
336
395
|
api,
|
|
337
396
|
trackingResult.trackedCall,
|
|
338
|
-
{ autoGenerateTags, staleTime }
|
|
397
|
+
{ autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
|
|
339
398
|
);
|
|
340
399
|
}
|
|
341
400
|
return useEnlaceHook;
|
|
@@ -10,6 +10,23 @@ type ReactRequestOptionsBase = {
|
|
|
10
10
|
tags?: string[];
|
|
11
11
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
12
12
|
revalidateTags?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Path parameters for dynamic URL segments.
|
|
15
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
16
|
+
* @example
|
|
17
|
+
* // With path api.products[':id'].delete
|
|
18
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
19
|
+
*/
|
|
20
|
+
pathParams?: Record<string, string | number>;
|
|
21
|
+
};
|
|
22
|
+
/** Options for query mode hooks */
|
|
23
|
+
type UseEnlaceQueryOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* Whether the query should execute.
|
|
26
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
enabled?: boolean;
|
|
13
30
|
};
|
|
14
31
|
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
15
32
|
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -101,7 +118,7 @@ type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextR
|
|
|
101
118
|
type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
|
|
102
119
|
type NextEnlaceHook<TSchema> = {
|
|
103
120
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
104
|
-
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError
|
|
121
|
+
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
105
122
|
};
|
|
106
123
|
/**
|
|
107
124
|
* Creates a React hook for making API calls in Next.js Client Components.
|
|
@@ -10,6 +10,23 @@ type ReactRequestOptionsBase = {
|
|
|
10
10
|
tags?: string[];
|
|
11
11
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
12
12
|
revalidateTags?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Path parameters for dynamic URL segments.
|
|
15
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
16
|
+
* @example
|
|
17
|
+
* // With path api.products[':id'].delete
|
|
18
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
19
|
+
*/
|
|
20
|
+
pathParams?: Record<string, string | number>;
|
|
21
|
+
};
|
|
22
|
+
/** Options for query mode hooks */
|
|
23
|
+
type UseEnlaceQueryOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* Whether the query should execute.
|
|
26
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
enabled?: boolean;
|
|
13
30
|
};
|
|
14
31
|
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
15
32
|
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -101,7 +118,7 @@ type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextR
|
|
|
101
118
|
type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
|
|
102
119
|
type NextEnlaceHook<TSchema> = {
|
|
103
120
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
104
|
-
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError
|
|
121
|
+
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
105
122
|
};
|
|
106
123
|
/**
|
|
107
124
|
* Creates a React hook for making API calls in Next.js Client Components.
|
package/dist/next/hook/index.js
CHANGED
|
@@ -257,11 +257,26 @@ function onRevalidate(callback) {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
// src/react/useQueryMode.ts
|
|
260
|
+
function resolvePath(path, pathParams) {
|
|
261
|
+
if (!pathParams) return path;
|
|
262
|
+
return path.map((segment) => {
|
|
263
|
+
if (segment.startsWith(":")) {
|
|
264
|
+
const paramName = segment.slice(1);
|
|
265
|
+
const value = pathParams[paramName];
|
|
266
|
+
if (value === void 0) {
|
|
267
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
268
|
+
}
|
|
269
|
+
return String(value);
|
|
270
|
+
}
|
|
271
|
+
return segment;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
260
274
|
function useQueryMode(api, trackedCall, options) {
|
|
261
|
-
const { autoGenerateTags, staleTime } = options;
|
|
275
|
+
const { autoGenerateTags, staleTime, enabled } = options;
|
|
262
276
|
const queryKey = createQueryKey(trackedCall);
|
|
263
277
|
const requestOptions = trackedCall.options;
|
|
264
|
-
const
|
|
278
|
+
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
|
|
279
|
+
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
265
280
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
266
281
|
const cached = getCache(queryKey);
|
|
267
282
|
const hasCachedData = cached?.data !== void 0;
|
|
@@ -280,6 +295,15 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
280
295
|
const fetchRef = (0, import_react.useRef)(null);
|
|
281
296
|
(0, import_react.useEffect)(() => {
|
|
282
297
|
mountedRef.current = true;
|
|
298
|
+
if (!enabled) {
|
|
299
|
+
dispatch({
|
|
300
|
+
type: "RESET",
|
|
301
|
+
state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
|
|
302
|
+
});
|
|
303
|
+
return () => {
|
|
304
|
+
mountedRef.current = false;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
283
307
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
284
308
|
const unsubscribe = subscribeCache(queryKey, () => {
|
|
285
309
|
if (mountedRef.current) {
|
|
@@ -293,7 +317,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
293
317
|
}
|
|
294
318
|
dispatch({ type: "FETCH_START" });
|
|
295
319
|
let current = api;
|
|
296
|
-
for (const segment of
|
|
320
|
+
for (const segment of resolvedPath) {
|
|
297
321
|
current = current[segment];
|
|
298
322
|
}
|
|
299
323
|
const method = current[trackedCall.method];
|
|
@@ -325,7 +349,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
325
349
|
fetchRef.current = null;
|
|
326
350
|
unsubscribe();
|
|
327
351
|
};
|
|
328
|
-
}, [queryKey]);
|
|
352
|
+
}, [queryKey, enabled]);
|
|
329
353
|
(0, import_react.useEffect)(() => {
|
|
330
354
|
if (queryTags.length === 0) return;
|
|
331
355
|
return onRevalidate((invalidatedTags) => {
|
|
@@ -340,23 +364,56 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
340
364
|
|
|
341
365
|
// src/react/useSelectorMode.ts
|
|
342
366
|
var import_react2 = require("react");
|
|
343
|
-
function
|
|
367
|
+
function resolvePath2(path, pathParams) {
|
|
368
|
+
if (!pathParams) return path;
|
|
369
|
+
return path.map((segment) => {
|
|
370
|
+
if (segment.startsWith(":")) {
|
|
371
|
+
const paramName = segment.slice(1);
|
|
372
|
+
const value = pathParams[paramName];
|
|
373
|
+
if (value === void 0) {
|
|
374
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
375
|
+
}
|
|
376
|
+
return String(value);
|
|
377
|
+
}
|
|
378
|
+
return segment;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function hasPathParams(path) {
|
|
382
|
+
return path.some((segment) => segment.startsWith(":"));
|
|
383
|
+
}
|
|
384
|
+
function useSelectorMode(config) {
|
|
385
|
+
const { method, api, path, methodName, autoRevalidateTags } = config;
|
|
344
386
|
const [state, dispatch] = (0, import_react2.useReducer)(hookReducer, initialState);
|
|
345
387
|
const methodRef = (0, import_react2.useRef)(method);
|
|
388
|
+
const apiRef = (0, import_react2.useRef)(api);
|
|
346
389
|
const triggerRef = (0, import_react2.useRef)(null);
|
|
347
390
|
const pathRef = (0, import_react2.useRef)(path);
|
|
391
|
+
const methodNameRef = (0, import_react2.useRef)(methodName);
|
|
348
392
|
const autoRevalidateRef = (0, import_react2.useRef)(autoRevalidateTags);
|
|
349
393
|
methodRef.current = method;
|
|
394
|
+
apiRef.current = api;
|
|
350
395
|
pathRef.current = path;
|
|
396
|
+
methodNameRef.current = methodName;
|
|
351
397
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
352
398
|
if (!triggerRef.current) {
|
|
353
399
|
triggerRef.current = (async (...args) => {
|
|
354
400
|
dispatch({ type: "FETCH_START" });
|
|
355
|
-
const
|
|
401
|
+
const options = args[0];
|
|
402
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
|
|
403
|
+
let res;
|
|
404
|
+
if (hasPathParams(pathRef.current)) {
|
|
405
|
+
let current = apiRef.current;
|
|
406
|
+
for (const segment of resolvedPath) {
|
|
407
|
+
current = current[segment];
|
|
408
|
+
}
|
|
409
|
+
const resolvedMethod = current[methodNameRef.current];
|
|
410
|
+
res = await resolvedMethod(...args);
|
|
411
|
+
} else {
|
|
412
|
+
res = await methodRef.current(...args);
|
|
413
|
+
}
|
|
356
414
|
if (res.ok) {
|
|
357
415
|
dispatch({ type: "FETCH_SUCCESS", data: res.data });
|
|
358
|
-
const
|
|
359
|
-
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
|
|
416
|
+
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
|
|
360
417
|
if (tagsToInvalidate.length > 0) {
|
|
361
418
|
invalidateTags(tagsToInvalidate);
|
|
362
419
|
}
|
|
@@ -417,26 +474,30 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
417
474
|
autoRevalidateTags,
|
|
418
475
|
...nextOptions
|
|
419
476
|
});
|
|
420
|
-
function useEnlaceHook(selectorOrQuery) {
|
|
477
|
+
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
421
478
|
let trackedCall = null;
|
|
422
479
|
let selectorPath = null;
|
|
480
|
+
let selectorMethod = null;
|
|
423
481
|
const trackingProxy = createTrackingProxy((result2) => {
|
|
424
482
|
trackedCall = result2.trackedCall;
|
|
425
483
|
selectorPath = result2.selectorPath;
|
|
484
|
+
selectorMethod = result2.selectorMethod;
|
|
426
485
|
});
|
|
427
486
|
const result = selectorOrQuery(trackingProxy);
|
|
428
487
|
if (typeof result === "function") {
|
|
429
488
|
const actualResult = selectorOrQuery(api);
|
|
430
|
-
return useSelectorMode(
|
|
431
|
-
actualResult,
|
|
432
|
-
|
|
489
|
+
return useSelectorMode({
|
|
490
|
+
method: actualResult,
|
|
491
|
+
api,
|
|
492
|
+
path: selectorPath ?? [],
|
|
493
|
+
methodName: selectorMethod ?? "",
|
|
433
494
|
autoRevalidateTags
|
|
434
|
-
);
|
|
495
|
+
});
|
|
435
496
|
}
|
|
436
497
|
return useQueryMode(
|
|
437
498
|
api,
|
|
438
499
|
trackedCall,
|
|
439
|
-
{ autoGenerateTags, staleTime }
|
|
500
|
+
{ autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
|
|
440
501
|
);
|
|
441
502
|
}
|
|
442
503
|
return useEnlaceHook;
|
package/dist/next/hook/index.mjs
CHANGED
|
@@ -255,11 +255,26 @@ function onRevalidate(callback) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// src/react/useQueryMode.ts
|
|
258
|
+
function resolvePath(path, pathParams) {
|
|
259
|
+
if (!pathParams) return path;
|
|
260
|
+
return path.map((segment) => {
|
|
261
|
+
if (segment.startsWith(":")) {
|
|
262
|
+
const paramName = segment.slice(1);
|
|
263
|
+
const value = pathParams[paramName];
|
|
264
|
+
if (value === void 0) {
|
|
265
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
266
|
+
}
|
|
267
|
+
return String(value);
|
|
268
|
+
}
|
|
269
|
+
return segment;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
258
272
|
function useQueryMode(api, trackedCall, options) {
|
|
259
|
-
const { autoGenerateTags, staleTime } = options;
|
|
273
|
+
const { autoGenerateTags, staleTime, enabled } = options;
|
|
260
274
|
const queryKey = createQueryKey(trackedCall);
|
|
261
275
|
const requestOptions = trackedCall.options;
|
|
262
|
-
const
|
|
276
|
+
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
|
|
277
|
+
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
263
278
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
264
279
|
const cached = getCache(queryKey);
|
|
265
280
|
const hasCachedData = cached?.data !== void 0;
|
|
@@ -278,6 +293,15 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
278
293
|
const fetchRef = useRef(null);
|
|
279
294
|
useEffect(() => {
|
|
280
295
|
mountedRef.current = true;
|
|
296
|
+
if (!enabled) {
|
|
297
|
+
dispatch({
|
|
298
|
+
type: "RESET",
|
|
299
|
+
state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
|
|
300
|
+
});
|
|
301
|
+
return () => {
|
|
302
|
+
mountedRef.current = false;
|
|
303
|
+
};
|
|
304
|
+
}
|
|
281
305
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
282
306
|
const unsubscribe = subscribeCache(queryKey, () => {
|
|
283
307
|
if (mountedRef.current) {
|
|
@@ -291,7 +315,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
291
315
|
}
|
|
292
316
|
dispatch({ type: "FETCH_START" });
|
|
293
317
|
let current = api;
|
|
294
|
-
for (const segment of
|
|
318
|
+
for (const segment of resolvedPath) {
|
|
295
319
|
current = current[segment];
|
|
296
320
|
}
|
|
297
321
|
const method = current[trackedCall.method];
|
|
@@ -323,7 +347,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
323
347
|
fetchRef.current = null;
|
|
324
348
|
unsubscribe();
|
|
325
349
|
};
|
|
326
|
-
}, [queryKey]);
|
|
350
|
+
}, [queryKey, enabled]);
|
|
327
351
|
useEffect(() => {
|
|
328
352
|
if (queryTags.length === 0) return;
|
|
329
353
|
return onRevalidate((invalidatedTags) => {
|
|
@@ -338,23 +362,56 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
338
362
|
|
|
339
363
|
// src/react/useSelectorMode.ts
|
|
340
364
|
import { useRef as useRef2, useReducer as useReducer2 } from "react";
|
|
341
|
-
function
|
|
365
|
+
function resolvePath2(path, pathParams) {
|
|
366
|
+
if (!pathParams) return path;
|
|
367
|
+
return path.map((segment) => {
|
|
368
|
+
if (segment.startsWith(":")) {
|
|
369
|
+
const paramName = segment.slice(1);
|
|
370
|
+
const value = pathParams[paramName];
|
|
371
|
+
if (value === void 0) {
|
|
372
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
373
|
+
}
|
|
374
|
+
return String(value);
|
|
375
|
+
}
|
|
376
|
+
return segment;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function hasPathParams(path) {
|
|
380
|
+
return path.some((segment) => segment.startsWith(":"));
|
|
381
|
+
}
|
|
382
|
+
function useSelectorMode(config) {
|
|
383
|
+
const { method, api, path, methodName, autoRevalidateTags } = config;
|
|
342
384
|
const [state, dispatch] = useReducer2(hookReducer, initialState);
|
|
343
385
|
const methodRef = useRef2(method);
|
|
386
|
+
const apiRef = useRef2(api);
|
|
344
387
|
const triggerRef = useRef2(null);
|
|
345
388
|
const pathRef = useRef2(path);
|
|
389
|
+
const methodNameRef = useRef2(methodName);
|
|
346
390
|
const autoRevalidateRef = useRef2(autoRevalidateTags);
|
|
347
391
|
methodRef.current = method;
|
|
392
|
+
apiRef.current = api;
|
|
348
393
|
pathRef.current = path;
|
|
394
|
+
methodNameRef.current = methodName;
|
|
349
395
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
350
396
|
if (!triggerRef.current) {
|
|
351
397
|
triggerRef.current = (async (...args) => {
|
|
352
398
|
dispatch({ type: "FETCH_START" });
|
|
353
|
-
const
|
|
399
|
+
const options = args[0];
|
|
400
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
|
|
401
|
+
let res;
|
|
402
|
+
if (hasPathParams(pathRef.current)) {
|
|
403
|
+
let current = apiRef.current;
|
|
404
|
+
for (const segment of resolvedPath) {
|
|
405
|
+
current = current[segment];
|
|
406
|
+
}
|
|
407
|
+
const resolvedMethod = current[methodNameRef.current];
|
|
408
|
+
res = await resolvedMethod(...args);
|
|
409
|
+
} else {
|
|
410
|
+
res = await methodRef.current(...args);
|
|
411
|
+
}
|
|
354
412
|
if (res.ok) {
|
|
355
413
|
dispatch({ type: "FETCH_SUCCESS", data: res.data });
|
|
356
|
-
const
|
|
357
|
-
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
|
|
414
|
+
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
|
|
358
415
|
if (tagsToInvalidate.length > 0) {
|
|
359
416
|
invalidateTags(tagsToInvalidate);
|
|
360
417
|
}
|
|
@@ -415,26 +472,30 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
415
472
|
autoRevalidateTags,
|
|
416
473
|
...nextOptions
|
|
417
474
|
});
|
|
418
|
-
function useEnlaceHook(selectorOrQuery) {
|
|
475
|
+
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
419
476
|
let trackedCall = null;
|
|
420
477
|
let selectorPath = null;
|
|
478
|
+
let selectorMethod = null;
|
|
421
479
|
const trackingProxy = createTrackingProxy((result2) => {
|
|
422
480
|
trackedCall = result2.trackedCall;
|
|
423
481
|
selectorPath = result2.selectorPath;
|
|
482
|
+
selectorMethod = result2.selectorMethod;
|
|
424
483
|
});
|
|
425
484
|
const result = selectorOrQuery(trackingProxy);
|
|
426
485
|
if (typeof result === "function") {
|
|
427
486
|
const actualResult = selectorOrQuery(api);
|
|
428
|
-
return useSelectorMode(
|
|
429
|
-
actualResult,
|
|
430
|
-
|
|
487
|
+
return useSelectorMode({
|
|
488
|
+
method: actualResult,
|
|
489
|
+
api,
|
|
490
|
+
path: selectorPath ?? [],
|
|
491
|
+
methodName: selectorMethod ?? "",
|
|
431
492
|
autoRevalidateTags
|
|
432
|
-
);
|
|
493
|
+
});
|
|
433
494
|
}
|
|
434
495
|
return useQueryMode(
|
|
435
496
|
api,
|
|
436
497
|
trackedCall,
|
|
437
|
-
{ autoGenerateTags, staleTime }
|
|
498
|
+
{ autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
|
|
438
499
|
);
|
|
439
500
|
}
|
|
440
501
|
return useEnlaceHook;
|
package/dist/next/index.d.mts
CHANGED
|
@@ -12,6 +12,14 @@ type ReactRequestOptionsBase = {
|
|
|
12
12
|
tags?: string[];
|
|
13
13
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
14
14
|
revalidateTags?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Path parameters for dynamic URL segments.
|
|
17
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
18
|
+
* @example
|
|
19
|
+
* // With path api.products[':id'].delete
|
|
20
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
21
|
+
*/
|
|
22
|
+
pathParams?: Record<string, string | number>;
|
|
15
23
|
};
|
|
16
24
|
|
|
17
25
|
type EnlaceHookOptions = {
|
package/dist/next/index.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ type ReactRequestOptionsBase = {
|
|
|
12
12
|
tags?: string[];
|
|
13
13
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
14
14
|
revalidateTags?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Path parameters for dynamic URL segments.
|
|
17
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
18
|
+
* @example
|
|
19
|
+
* // With path api.products[':id'].delete
|
|
20
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
21
|
+
*/
|
|
22
|
+
pathParams?: Record<string, string | number>;
|
|
15
23
|
};
|
|
16
24
|
|
|
17
25
|
type EnlaceHookOptions = {
|