@spoosh/react 0.11.0 → 0.12.0
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 +58 -41
- package/dist/index.d.mts +89 -94
- package/dist/index.d.ts +89 -94
- package/dist/index.js +59 -51
- package/dist/index.mjs +61 -52
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @spoosh/react
|
|
2
2
|
|
|
3
|
-
React hooks for Spoosh - `useRead`, `useWrite`, and `
|
|
3
|
+
React hooks for Spoosh - `useRead`, `useWrite`, and `usePages`.
|
|
4
4
|
|
|
5
5
|
**[Documentation](https://spoosh.dev/docs/react)** · **Requirements:** TypeScript >= 5.0, React >= 18.0
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
|
|
|
23
23
|
cachePlugin({ staleTime: 5000 }),
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
-
export const { useRead, useWrite,
|
|
26
|
+
export const { useRead, useWrite, usePages } = create(spoosh);
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### useRead
|
|
@@ -98,7 +98,7 @@ await updateUser.trigger({
|
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
###
|
|
101
|
+
### usePages
|
|
102
102
|
|
|
103
103
|
Bidirectional paginated data fetching with infinite scroll support.
|
|
104
104
|
|
|
@@ -106,7 +106,7 @@ Bidirectional paginated data fetching with infinite scroll support.
|
|
|
106
106
|
function PostList() {
|
|
107
107
|
const {
|
|
108
108
|
data,
|
|
109
|
-
|
|
109
|
+
pages,
|
|
110
110
|
loading,
|
|
111
111
|
canFetchNext,
|
|
112
112
|
canFetchPrev,
|
|
@@ -114,26 +114,26 @@ function PostList() {
|
|
|
114
114
|
fetchPrev,
|
|
115
115
|
fetchingNext,
|
|
116
116
|
fetchingPrev,
|
|
117
|
-
} =
|
|
117
|
+
} = usePages(
|
|
118
118
|
(api) => api("posts").GET({ query: { page: 1 } }),
|
|
119
119
|
{
|
|
120
120
|
// Required: Check if next page exists
|
|
121
|
-
canFetchNext: ({
|
|
121
|
+
canFetchNext: ({ lastPage }) => lastPage?.data?.meta.hasMore ?? false,
|
|
122
122
|
|
|
123
123
|
// Required: Build request for next page
|
|
124
|
-
nextPageRequest: ({
|
|
125
|
-
query: {
|
|
124
|
+
nextPageRequest: ({ lastPage }) => ({
|
|
125
|
+
query: { page: (lastPage?.data?.meta.page ?? 0) + 1 },
|
|
126
126
|
}),
|
|
127
127
|
|
|
128
|
-
// Required: Merge all
|
|
129
|
-
merger: (
|
|
128
|
+
// Required: Merge all pages into items
|
|
129
|
+
merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
|
|
130
130
|
|
|
131
131
|
// Optional: Check if previous page exists
|
|
132
|
-
canFetchPrev: ({
|
|
132
|
+
canFetchPrev: ({ firstPage }) => (firstPage?.data?.meta.page ?? 1) > 1,
|
|
133
133
|
|
|
134
134
|
// Optional: Build request for previous page
|
|
135
|
-
prevPageRequest: ({
|
|
136
|
-
query: {
|
|
135
|
+
prevPageRequest: ({ firstPage }) => ({
|
|
136
|
+
query: { page: (firstPage?.data?.meta.page ?? 2) - 1 },
|
|
137
137
|
}),
|
|
138
138
|
}
|
|
139
139
|
);
|
|
@@ -192,41 +192,58 @@ function PostList() {
|
|
|
192
192
|
| `loading` | `boolean` | True while mutation is in progress |
|
|
193
193
|
| `abort` | `() => void` | Abort current request |
|
|
194
194
|
|
|
195
|
-
###
|
|
195
|
+
### usePages(readFn, options)
|
|
196
196
|
|
|
197
|
-
| Option | Type | Required | Description
|
|
198
|
-
| ----------------- | ---------------------------- | -------- |
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `canFetchPrev` | `(ctx) => boolean` | No | Check if previous page exists
|
|
203
|
-
| `prevPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for previous page
|
|
204
|
-
| `enabled` | `boolean` | No | Whether to fetch automatically
|
|
197
|
+
| Option | Type | Required | Description |
|
|
198
|
+
| ----------------- | ---------------------------- | -------- | ------------------------------------------------- |
|
|
199
|
+
| `merger` | `(pages) => TItem[]` | Yes | Merge all pages into items |
|
|
200
|
+
| `canFetchNext` | `(ctx) => boolean` | No | Check if next page exists. Default: `() => false` |
|
|
201
|
+
| `nextPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for next page |
|
|
202
|
+
| `canFetchPrev` | `(ctx) => boolean` | No | Check if previous page exists |
|
|
203
|
+
| `prevPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for previous page |
|
|
204
|
+
| `enabled` | `boolean` | No | Whether to fetch automatically |
|
|
205
205
|
|
|
206
206
|
**Context object passed to callbacks:**
|
|
207
207
|
|
|
208
208
|
```typescript
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
// For canFetchNext and nextPageRequest
|
|
210
|
+
type NextContext<TData, TRequest> = {
|
|
211
|
+
lastPage: InfinitePage<TData> | undefined;
|
|
212
|
+
pages: InfinitePage<TData>[];
|
|
213
|
+
request: TRequest;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// For canFetchPrev and prevPageRequest
|
|
217
|
+
type PrevContext<TData, TRequest> = {
|
|
218
|
+
firstPage: InfinitePage<TData> | undefined;
|
|
219
|
+
pages: InfinitePage<TData>[];
|
|
220
|
+
request: TRequest;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Each page in the pages array
|
|
224
|
+
type InfinitePage<TData> = {
|
|
225
|
+
status: "pending" | "loading" | "success" | "error" | "stale";
|
|
226
|
+
data?: TData;
|
|
227
|
+
error?: TError;
|
|
228
|
+
meta?: TMeta;
|
|
229
|
+
input?: { query?; params?; body? };
|
|
213
230
|
};
|
|
214
231
|
```
|
|
215
232
|
|
|
216
233
|
**Returns:**
|
|
217
234
|
|
|
218
|
-
| Property | Type
|
|
219
|
-
| -------------- |
|
|
220
|
-
| `data` | `TItem[] \| undefined`
|
|
221
|
-
| `
|
|
222
|
-
| `loading` | `boolean`
|
|
223
|
-
| `fetching` | `boolean`
|
|
224
|
-
| `fetchingNext` | `boolean`
|
|
225
|
-
| `fetchingPrev` | `boolean`
|
|
226
|
-
| `canFetchNext` | `boolean`
|
|
227
|
-
| `canFetchPrev` | `boolean`
|
|
228
|
-
| `fetchNext` | `() => Promise<void>`
|
|
229
|
-
| `fetchPrev` | `() => Promise<void>`
|
|
230
|
-
| `
|
|
231
|
-
| `abort` | `() => void`
|
|
232
|
-
| `error` | `TError \| undefined`
|
|
235
|
+
| Property | Type | Description |
|
|
236
|
+
| -------------- | ----------------------------- | ----------------------------------------------- |
|
|
237
|
+
| `data` | `TItem[] \| undefined` | Merged items from all pages |
|
|
238
|
+
| `pages` | `InfinitePage<TData>[]` | Array of all pages with status, data, and meta |
|
|
239
|
+
| `loading` | `boolean` | True during initial load |
|
|
240
|
+
| `fetching` | `boolean` | True during any fetch |
|
|
241
|
+
| `fetchingNext` | `boolean` | True while fetching next page |
|
|
242
|
+
| `fetchingPrev` | `boolean` | True while fetching previous |
|
|
243
|
+
| `canFetchNext` | `boolean` | Whether next page exists |
|
|
244
|
+
| `canFetchPrev` | `boolean` | Whether previous page exists |
|
|
245
|
+
| `fetchNext` | `() => Promise<void>` | Fetch the next page |
|
|
246
|
+
| `fetchPrev` | `() => Promise<void>` | Fetch the previous page |
|
|
247
|
+
| `trigger` | `(options?) => Promise<void>` | Trigger fetch with optional new request options |
|
|
248
|
+
| `abort` | `() => void` | Abort current request |
|
|
249
|
+
| `error` | `TError \| undefined` | Error if request failed |
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReadClient, TagMode, SpooshResponse, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
|
|
1
|
+
import { ReadClient, TagMode, SpooshResponse, ExtractTriggerQuery as ExtractTriggerQuery$2, ExtractTriggerBody as ExtractTriggerBody$2, ExtractTriggerParams as ExtractTriggerParams$2, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, InfiniteRequestOptions, InfiniteNextContext, InfinitePage, InfinitePrevContext, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
|
|
2
2
|
|
|
3
3
|
type SuccessResponse<T> = Extract<T, {
|
|
4
4
|
data: unknown;
|
|
@@ -81,26 +81,11 @@ type ResponseInputFields<TQuery, TBody, TParamNames extends string> = [
|
|
|
81
81
|
] extends [never, never, never] ? object : {
|
|
82
82
|
input: ReadInputFields<TQuery, TBody, TParamNames>;
|
|
83
83
|
};
|
|
84
|
-
type TriggerAwaitedReturn$
|
|
85
|
-
type ExtractInputFromResponse$
|
|
84
|
+
type TriggerAwaitedReturn$3<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
|
|
85
|
+
type ExtractInputFromResponse$3<T> = T extends {
|
|
86
86
|
input: infer I;
|
|
87
87
|
} ? I : never;
|
|
88
|
-
type
|
|
89
|
-
query: infer Q;
|
|
90
|
-
} ? {
|
|
91
|
-
query?: Q;
|
|
92
|
-
} : unknown;
|
|
93
|
-
type ExtractTriggerBody$2<I> = I extends {
|
|
94
|
-
body: infer B;
|
|
95
|
-
} ? {
|
|
96
|
-
body?: B;
|
|
97
|
-
} : unknown;
|
|
98
|
-
type ExtractTriggerParams$2<I> = I extends {
|
|
99
|
-
params: infer P;
|
|
100
|
-
} ? {
|
|
101
|
-
params?: P;
|
|
102
|
-
} : unknown;
|
|
103
|
-
type TriggerOptions<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? {
|
|
88
|
+
type TriggerOptions<T> = ExtractInputFromResponse$3<TriggerAwaitedReturn$3<T>> extends infer I ? [I] extends [never] ? {
|
|
104
89
|
force?: boolean;
|
|
105
90
|
} : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & {
|
|
106
91
|
/** Force refetch even if data is cached */
|
|
@@ -160,8 +145,8 @@ type InputFields<TQuery, TBody, TParamNames extends string> = OptionalQueryField
|
|
|
160
145
|
type WriteResponseInputFields<TQuery, TBody, TParamNames extends string> = [TQuery, TBody, TParamNames] extends [never, never, never] ? object : {
|
|
161
146
|
input: InputFields<TQuery, TBody, TParamNames> | undefined;
|
|
162
147
|
};
|
|
163
|
-
type TriggerAwaitedReturn$
|
|
164
|
-
type ExtractInputFromResponse$
|
|
148
|
+
type TriggerAwaitedReturn$2<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
|
|
149
|
+
type ExtractInputFromResponse$2<T> = T extends {
|
|
165
150
|
input: infer I;
|
|
166
151
|
} ? I : never;
|
|
167
152
|
type ExtractTriggerQuery$1<I> = I extends {
|
|
@@ -183,7 +168,7 @@ type ExtractTriggerParams$1<I> = I extends {
|
|
|
183
168
|
} ? {
|
|
184
169
|
params: P;
|
|
185
170
|
} : unknown;
|
|
186
|
-
type WriteTriggerInput<T> = ExtractInputFromResponse$
|
|
171
|
+
type WriteTriggerInput<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : object;
|
|
187
172
|
/**
|
|
188
173
|
* Result returned by `useWrite` hook.
|
|
189
174
|
*
|
|
@@ -209,8 +194,8 @@ type BaseWriteResult<TData, TError, TOptions, TMeta = Record<string, unknown>> =
|
|
|
209
194
|
type UseWriteResult<TData, TError, TOptions, TMeta, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseWriteResult<TData, TError, TOptions, TMeta> & MergePluginResults<TPlugins>["write"];
|
|
210
195
|
type WriteApiClient<TSchema, TDefaultError> = WriteSelectorClient<TSchema, TDefaultError>;
|
|
211
196
|
|
|
212
|
-
type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
|
|
213
|
-
type ExtractInputFromResponse<T> = T extends {
|
|
197
|
+
type TriggerAwaitedReturn$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
|
|
198
|
+
type ExtractInputFromResponse$1<T> = T extends {
|
|
214
199
|
input: infer I;
|
|
215
200
|
} ? I : never;
|
|
216
201
|
type ExtractTriggerQuery<I> = I extends {
|
|
@@ -236,7 +221,7 @@ type QueueTriggerBase = {
|
|
|
236
221
|
/** Custom ID for this queue item. If not provided, one will be auto-generated. */
|
|
237
222
|
id?: string;
|
|
238
223
|
};
|
|
239
|
-
type QueueTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
|
|
224
|
+
type QueueTriggerInput<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
|
|
240
225
|
/**
|
|
241
226
|
* Options for useQueue hook.
|
|
242
227
|
*/
|
|
@@ -277,41 +262,25 @@ type UseQueueResult<TData, TError, TTriggerInput, TMeta = object> = {
|
|
|
277
262
|
type QueueApiClient<TSchema, TDefaultError> = QueueSelectorClient<TSchema, TDefaultError>;
|
|
278
263
|
|
|
279
264
|
type TagModeInArray = "all" | "self";
|
|
280
|
-
type
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
*/
|
|
288
|
-
type InfiniteNextContext<TData, TRequest> = {
|
|
289
|
-
/** The latest fetched response data */
|
|
290
|
-
response: TData | undefined;
|
|
291
|
-
/** All responses fetched so far */
|
|
292
|
-
allResponses: TData[];
|
|
293
|
-
/** The current request options (query, params, body) */
|
|
294
|
-
request: TRequest;
|
|
295
|
-
};
|
|
296
|
-
/**
|
|
297
|
-
* Context passed to `canFetchPrev` and `prevPageRequest` callbacks.
|
|
298
|
-
*/
|
|
299
|
-
type InfinitePrevContext<TData, TRequest> = {
|
|
300
|
-
/** The latest fetched response data */
|
|
301
|
-
response: TData | undefined;
|
|
302
|
-
/** All responses fetched so far */
|
|
303
|
-
allResponses: TData[];
|
|
304
|
-
/** The current request options (query, params, body) */
|
|
305
|
-
request: TRequest;
|
|
265
|
+
type TriggerAwaitedReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
|
|
266
|
+
type ExtractInputFromResponse<T> = T extends {
|
|
267
|
+
input: infer I;
|
|
268
|
+
} ? I : never;
|
|
269
|
+
type BaseTriggerOptions = {
|
|
270
|
+
/** Bypass cache and force refetch. Default: true */
|
|
271
|
+
force?: boolean;
|
|
306
272
|
};
|
|
273
|
+
type PagesTriggerOptions<TReadFn> = ExtractInputFromResponse<TriggerAwaitedReturn<TReadFn>> extends infer I ? [I] extends [never] ? BaseTriggerOptions : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & BaseTriggerOptions : BaseTriggerOptions;
|
|
307
274
|
/**
|
|
308
|
-
* Options for `
|
|
275
|
+
* Options for `usePages` hook.
|
|
309
276
|
*
|
|
310
277
|
* @template TData - The response data type for each page
|
|
311
278
|
* @template TItem - The item type after merging all responses
|
|
279
|
+
* @template TError - The error type
|
|
312
280
|
* @template TRequest - The request options type (query, params, body)
|
|
281
|
+
* @template TMeta - Plugin metadata type
|
|
313
282
|
*/
|
|
314
|
-
type
|
|
283
|
+
type BasePagesOptions<TData, TItem, TError = unknown, TRequest = InfiniteRequestOptions, TMeta = Record<string, unknown>> = {
|
|
315
284
|
/** Whether to fetch automatically on mount. Default: true */
|
|
316
285
|
enabled?: boolean;
|
|
317
286
|
/**
|
|
@@ -322,30 +291,66 @@ type BaseInfiniteReadOptions<TData, TItem, TRequest = AnyInfiniteRequestOptions>
|
|
|
322
291
|
* - 'none' should only be used as string (use `tags: 'none'` not in array)
|
|
323
292
|
*/
|
|
324
293
|
tags?: TagMode | (TagModeInArray | (string & {}))[];
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Callback to determine if there's a next page to fetch.
|
|
296
|
+
* Receives the last page to check for pagination indicators.
|
|
297
|
+
* Default: `() => false` (no next page fetching)
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* canFetchNext: ({ lastPage }) => lastPage?.data?.nextCursor != null
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
canFetchNext?: (ctx: InfiniteNextContext<TData, TError, TRequest, TMeta>) => boolean;
|
|
305
|
+
/**
|
|
306
|
+
* Callback to build the request options for the next page.
|
|
307
|
+
* Return only the fields that change - they will be **merged** with the initial request.
|
|
308
|
+
* Default: `() => ({})` (no changes to request)
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* // Initial: { query: { cursor: 0, limit: 10 } }
|
|
313
|
+
* // Only return cursor - limit is preserved automatically
|
|
314
|
+
* nextPageRequest: ({ lastPage }) => ({
|
|
315
|
+
* query: { cursor: lastPage?.data?.nextCursor }
|
|
316
|
+
* })
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
nextPageRequest?: (ctx: InfiniteNextContext<TData, TError, TRequest, TMeta>) => Partial<TRequest>;
|
|
320
|
+
/**
|
|
321
|
+
* Callback to merge all pages into a single array of items.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
merger: (pages: InfinitePage<TData, TError, TMeta>[]) => TItem[];
|
|
329
|
+
/**
|
|
330
|
+
* Callback to determine if there's a previous page to fetch.
|
|
331
|
+
* Receives the first page to check for pagination indicators.
|
|
332
|
+
*/
|
|
333
|
+
canFetchPrev?: (ctx: InfinitePrevContext<TData, TError, TRequest, TMeta>) => boolean;
|
|
334
|
+
/**
|
|
335
|
+
* Callback to build the request options for the previous page.
|
|
336
|
+
* Return only the fields that change - they will be **merged** with the initial request.
|
|
337
|
+
*/
|
|
338
|
+
prevPageRequest?: (ctx: InfinitePrevContext<TData, TError, TRequest, TMeta>) => Partial<TRequest>;
|
|
335
339
|
};
|
|
336
340
|
/**
|
|
337
|
-
* Result returned by `
|
|
341
|
+
* Result returned by `usePages` hook.
|
|
338
342
|
*
|
|
339
343
|
* @template TData - The response data type for each page
|
|
340
344
|
* @template TError - The error type
|
|
341
345
|
* @template TItem - The item type after merging all responses
|
|
342
346
|
* @template TPluginResult - Plugin-provided result fields
|
|
347
|
+
* @template TTriggerOptions - Options that can be passed to trigger()
|
|
343
348
|
*/
|
|
344
|
-
type
|
|
345
|
-
/** Merged items from all fetched
|
|
349
|
+
type BasePagesResult<TData, TError, TItem, TPluginResult = Record<string, unknown>, TTriggerOptions = object> = {
|
|
350
|
+
/** Merged items from all fetched pages */
|
|
346
351
|
data: TItem[] | undefined;
|
|
347
|
-
/** Array of all
|
|
348
|
-
|
|
352
|
+
/** Array of all pages with status, data, error, and meta per page */
|
|
353
|
+
pages: InfinitePage<TData, TError, TPluginResult>[];
|
|
349
354
|
/** True during the initial load (no data yet) */
|
|
350
355
|
loading: boolean;
|
|
351
356
|
/** True during any fetch operation */
|
|
@@ -358,21 +363,19 @@ type BaseInfiniteReadResult<TData, TError, TItem, TPluginResult = Record<string,
|
|
|
358
363
|
canFetchNext: boolean;
|
|
359
364
|
/** Whether there's a previous page available to fetch */
|
|
360
365
|
canFetchPrev: boolean;
|
|
361
|
-
/** Plugin-provided metadata */
|
|
362
|
-
meta: TPluginResult;
|
|
363
366
|
/** Fetch the next page */
|
|
364
367
|
fetchNext: () => Promise<void>;
|
|
365
368
|
/** Fetch the previous page */
|
|
366
369
|
fetchPrev: () => Promise<void>;
|
|
367
|
-
/** Trigger refetch of all pages from the beginning */
|
|
368
|
-
trigger: () => Promise<void>;
|
|
370
|
+
/** Trigger refetch of all pages from the beginning, optionally with new request options */
|
|
371
|
+
trigger: (options?: TTriggerOptions) => Promise<void>;
|
|
369
372
|
/** Abort the current fetch operation */
|
|
370
373
|
abort: () => void;
|
|
371
374
|
/** Error from the last failed request */
|
|
372
375
|
error: TError | undefined;
|
|
373
376
|
};
|
|
374
|
-
type
|
|
375
|
-
type
|
|
377
|
+
type UsePagesResult<TData, TError, TItem, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TTriggerOptions = object> = BasePagesResult<TData, TError, TItem, MergePluginResults<TPlugins>["read"], TTriggerOptions>;
|
|
378
|
+
type PagesApiClient<TSchema, TDefaultError> = ReadClient<TSchema, TDefaultError>;
|
|
376
379
|
|
|
377
380
|
type InferError<T, TDefaultError> = [T] extends [unknown] ? TDefaultError : T;
|
|
378
381
|
type WriteResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
|
|
@@ -391,17 +394,9 @@ type UseWriteFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
|
|
|
391
394
|
type UseQueueFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
|
|
392
395
|
<TQueueFn extends (api: QueueApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>>(queueFn: TQueueFn, queueOptions?: ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions): UseQueueResult<ExtractMethodData<TQueueFn>, InferError<ExtractMethodError<TQueueFn>, TDefaultError>, QueueTriggerInput<TQueueFn> & ResolvedQueueTriggerOptions<TSchema, TPlugins, TQueueFn, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["queue"], ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions>>;
|
|
393
396
|
};
|
|
394
|
-
type
|
|
395
|
-
query: infer Q;
|
|
396
|
-
} ? Q : never, TRequest extends {
|
|
397
|
-
body: infer B;
|
|
398
|
-
} ? B : never, TRequest extends {
|
|
399
|
-
params: infer P;
|
|
400
|
-
} ? P : never>;
|
|
401
|
-
type ResolvedInfiniteReadOptions<TSchema, TPlugins extends PluginArray, TData, TError, TRequest> = ResolveTypes<MergePluginOptions<TPlugins>["infiniteRead"], InfiniteReadResolverContext<TSchema, TData, TError, TRequest>>;
|
|
402
|
-
type UseInfiniteReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TData, TItem, TError = TDefaultError, TRequest extends AnyInfiniteRequestOptions = AnyInfiniteRequestOptions>(readFn: (api: InfiniteReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<TData, TError>>, readOptions: BaseInfiniteReadOptions<TData, TItem, TRequest> & ResolvedInfiniteReadOptions<TSchema, TPlugins, TData, TError, TRequest>) => BaseInfiniteReadResult<TData, TError, TItem, MergePluginResults<TPlugins>["read"]>;
|
|
397
|
+
type UsePagesFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TReadFn extends (api: PagesApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions, TItem = unknown>(readFn: TReadFn, readOptions: BasePagesOptions<ExtractMethodData<TReadFn>, TItem, InferError<ExtractMethodError<TReadFn>, TDefaultError>, TRequest, MergePluginResults<TPlugins>["read"]> & ResolveTypes<MergePluginOptions<TPlugins>["pages"], ResolverContext<TSchema, ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>>>) => BasePagesResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, TItem, MergePluginResults<TPlugins>["read"], PagesTriggerOptions<TReadFn>>;
|
|
403
398
|
/**
|
|
404
|
-
* Spoosh React hooks interface containing useRead, useWrite, and
|
|
399
|
+
* Spoosh React hooks interface containing useRead, useWrite, and usePages.
|
|
405
400
|
*
|
|
406
401
|
* @template TDefaultError - The default error type
|
|
407
402
|
* @template TSchema - The API schema type
|
|
@@ -448,21 +443,21 @@ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
|
|
|
448
443
|
*
|
|
449
444
|
* @param readFn - Function that selects the API endpoint to call
|
|
450
445
|
* @param readOptions - Configuration including `canFetchNext`, `nextPageRequest`, `merger`, and optional `canFetchPrev`/`prevPageRequest`
|
|
451
|
-
* @returns Object containing `data`, `
|
|
446
|
+
* @returns Object containing `data`, `pages`, `fetchNext`, `fetchPrev`, `canFetchNext`, `canFetchPrev`, `loading`, `fetching`, and pagination states
|
|
452
447
|
*
|
|
453
448
|
* @example
|
|
454
449
|
* ```tsx
|
|
455
|
-
* const { data, fetchNext, canFetchNext, loading } =
|
|
450
|
+
* const { data, fetchNext, canFetchNext, loading } = usePages(
|
|
456
451
|
* (api) => api("posts").GET(),
|
|
457
452
|
* {
|
|
458
|
-
* canFetchNext: ({
|
|
459
|
-
* nextPageRequest: ({
|
|
460
|
-
* merger: (
|
|
453
|
+
* canFetchNext: ({ lastPage }) => !!lastPage?.data?.nextCursor,
|
|
454
|
+
* nextPageRequest: ({ lastPage }) => ({ query: { cursor: lastPage?.data?.nextCursor } }),
|
|
455
|
+
* merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
|
|
461
456
|
* }
|
|
462
457
|
* );
|
|
463
458
|
* ```
|
|
464
459
|
*/
|
|
465
|
-
|
|
460
|
+
usePages: UsePagesFn<TDefaultError, TSchema, TPlugins>;
|
|
466
461
|
/**
|
|
467
462
|
* React hook for queued operations with concurrency control.
|
|
468
463
|
*
|
|
@@ -505,18 +500,18 @@ type SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins> = {
|
|
|
505
500
|
};
|
|
506
501
|
|
|
507
502
|
/**
|
|
508
|
-
* Creates React hooks (useRead, useWrite,
|
|
503
|
+
* Creates React hooks (useRead, useWrite, usePages) from a Spoosh instance.
|
|
509
504
|
*
|
|
510
505
|
* @template TSchema - The API schema type
|
|
511
506
|
* @template TDefaultError - The default error type
|
|
512
507
|
* @template TPlugins - The plugins array type
|
|
513
508
|
* @template TApi - The API type
|
|
514
509
|
* @param instance - The Spoosh instance containing api, stateManager, eventEmitter, and pluginExecutor
|
|
515
|
-
* @returns An object containing useRead, useWrite,
|
|
510
|
+
* @returns An object containing useRead, useWrite, usePages hooks and plugin instance APIs
|
|
516
511
|
*
|
|
517
512
|
* @example
|
|
518
513
|
* ```ts
|
|
519
|
-
* const { useRead, useWrite,
|
|
514
|
+
* const { useRead, useWrite, usePages } = create(spooshInstance);
|
|
520
515
|
*
|
|
521
516
|
* function MyComponent() {
|
|
522
517
|
* const { data, loading } = useRead((api) => api("posts").GET());
|
|
@@ -532,4 +527,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
|
|
|
532
527
|
plugins: TPlugins;
|
|
533
528
|
};
|
|
534
529
|
|
|
535
|
-
export { type
|
|
530
|
+
export { type BasePagesOptions, type BasePagesResult, type BaseReadOptions, type BaseReadResult, type BaseWriteResult, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReadClient, TagMode, SpooshResponse, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
|
|
1
|
+
import { ReadClient, TagMode, SpooshResponse, ExtractTriggerQuery as ExtractTriggerQuery$2, ExtractTriggerBody as ExtractTriggerBody$2, ExtractTriggerParams as ExtractTriggerParams$2, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, InfiniteRequestOptions, InfiniteNextContext, InfinitePage, InfinitePrevContext, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
|
|
2
2
|
|
|
3
3
|
type SuccessResponse<T> = Extract<T, {
|
|
4
4
|
data: unknown;
|
|
@@ -81,26 +81,11 @@ type ResponseInputFields<TQuery, TBody, TParamNames extends string> = [
|
|
|
81
81
|
] extends [never, never, never] ? object : {
|
|
82
82
|
input: ReadInputFields<TQuery, TBody, TParamNames>;
|
|
83
83
|
};
|
|
84
|
-
type TriggerAwaitedReturn$
|
|
85
|
-
type ExtractInputFromResponse$
|
|
84
|
+
type TriggerAwaitedReturn$3<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
|
|
85
|
+
type ExtractInputFromResponse$3<T> = T extends {
|
|
86
86
|
input: infer I;
|
|
87
87
|
} ? I : never;
|
|
88
|
-
type
|
|
89
|
-
query: infer Q;
|
|
90
|
-
} ? {
|
|
91
|
-
query?: Q;
|
|
92
|
-
} : unknown;
|
|
93
|
-
type ExtractTriggerBody$2<I> = I extends {
|
|
94
|
-
body: infer B;
|
|
95
|
-
} ? {
|
|
96
|
-
body?: B;
|
|
97
|
-
} : unknown;
|
|
98
|
-
type ExtractTriggerParams$2<I> = I extends {
|
|
99
|
-
params: infer P;
|
|
100
|
-
} ? {
|
|
101
|
-
params?: P;
|
|
102
|
-
} : unknown;
|
|
103
|
-
type TriggerOptions<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? {
|
|
88
|
+
type TriggerOptions<T> = ExtractInputFromResponse$3<TriggerAwaitedReturn$3<T>> extends infer I ? [I] extends [never] ? {
|
|
104
89
|
force?: boolean;
|
|
105
90
|
} : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & {
|
|
106
91
|
/** Force refetch even if data is cached */
|
|
@@ -160,8 +145,8 @@ type InputFields<TQuery, TBody, TParamNames extends string> = OptionalQueryField
|
|
|
160
145
|
type WriteResponseInputFields<TQuery, TBody, TParamNames extends string> = [TQuery, TBody, TParamNames] extends [never, never, never] ? object : {
|
|
161
146
|
input: InputFields<TQuery, TBody, TParamNames> | undefined;
|
|
162
147
|
};
|
|
163
|
-
type TriggerAwaitedReturn$
|
|
164
|
-
type ExtractInputFromResponse$
|
|
148
|
+
type TriggerAwaitedReturn$2<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
|
|
149
|
+
type ExtractInputFromResponse$2<T> = T extends {
|
|
165
150
|
input: infer I;
|
|
166
151
|
} ? I : never;
|
|
167
152
|
type ExtractTriggerQuery$1<I> = I extends {
|
|
@@ -183,7 +168,7 @@ type ExtractTriggerParams$1<I> = I extends {
|
|
|
183
168
|
} ? {
|
|
184
169
|
params: P;
|
|
185
170
|
} : unknown;
|
|
186
|
-
type WriteTriggerInput<T> = ExtractInputFromResponse$
|
|
171
|
+
type WriteTriggerInput<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : object;
|
|
187
172
|
/**
|
|
188
173
|
* Result returned by `useWrite` hook.
|
|
189
174
|
*
|
|
@@ -209,8 +194,8 @@ type BaseWriteResult<TData, TError, TOptions, TMeta = Record<string, unknown>> =
|
|
|
209
194
|
type UseWriteResult<TData, TError, TOptions, TMeta, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseWriteResult<TData, TError, TOptions, TMeta> & MergePluginResults<TPlugins>["write"];
|
|
210
195
|
type WriteApiClient<TSchema, TDefaultError> = WriteSelectorClient<TSchema, TDefaultError>;
|
|
211
196
|
|
|
212
|
-
type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
|
|
213
|
-
type ExtractInputFromResponse<T> = T extends {
|
|
197
|
+
type TriggerAwaitedReturn$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
|
|
198
|
+
type ExtractInputFromResponse$1<T> = T extends {
|
|
214
199
|
input: infer I;
|
|
215
200
|
} ? I : never;
|
|
216
201
|
type ExtractTriggerQuery<I> = I extends {
|
|
@@ -236,7 +221,7 @@ type QueueTriggerBase = {
|
|
|
236
221
|
/** Custom ID for this queue item. If not provided, one will be auto-generated. */
|
|
237
222
|
id?: string;
|
|
238
223
|
};
|
|
239
|
-
type QueueTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
|
|
224
|
+
type QueueTriggerInput<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
|
|
240
225
|
/**
|
|
241
226
|
* Options for useQueue hook.
|
|
242
227
|
*/
|
|
@@ -277,41 +262,25 @@ type UseQueueResult<TData, TError, TTriggerInput, TMeta = object> = {
|
|
|
277
262
|
type QueueApiClient<TSchema, TDefaultError> = QueueSelectorClient<TSchema, TDefaultError>;
|
|
278
263
|
|
|
279
264
|
type TagModeInArray = "all" | "self";
|
|
280
|
-
type
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
*/
|
|
288
|
-
type InfiniteNextContext<TData, TRequest> = {
|
|
289
|
-
/** The latest fetched response data */
|
|
290
|
-
response: TData | undefined;
|
|
291
|
-
/** All responses fetched so far */
|
|
292
|
-
allResponses: TData[];
|
|
293
|
-
/** The current request options (query, params, body) */
|
|
294
|
-
request: TRequest;
|
|
295
|
-
};
|
|
296
|
-
/**
|
|
297
|
-
* Context passed to `canFetchPrev` and `prevPageRequest` callbacks.
|
|
298
|
-
*/
|
|
299
|
-
type InfinitePrevContext<TData, TRequest> = {
|
|
300
|
-
/** The latest fetched response data */
|
|
301
|
-
response: TData | undefined;
|
|
302
|
-
/** All responses fetched so far */
|
|
303
|
-
allResponses: TData[];
|
|
304
|
-
/** The current request options (query, params, body) */
|
|
305
|
-
request: TRequest;
|
|
265
|
+
type TriggerAwaitedReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
|
|
266
|
+
type ExtractInputFromResponse<T> = T extends {
|
|
267
|
+
input: infer I;
|
|
268
|
+
} ? I : never;
|
|
269
|
+
type BaseTriggerOptions = {
|
|
270
|
+
/** Bypass cache and force refetch. Default: true */
|
|
271
|
+
force?: boolean;
|
|
306
272
|
};
|
|
273
|
+
type PagesTriggerOptions<TReadFn> = ExtractInputFromResponse<TriggerAwaitedReturn<TReadFn>> extends infer I ? [I] extends [never] ? BaseTriggerOptions : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & BaseTriggerOptions : BaseTriggerOptions;
|
|
307
274
|
/**
|
|
308
|
-
* Options for `
|
|
275
|
+
* Options for `usePages` hook.
|
|
309
276
|
*
|
|
310
277
|
* @template TData - The response data type for each page
|
|
311
278
|
* @template TItem - The item type after merging all responses
|
|
279
|
+
* @template TError - The error type
|
|
312
280
|
* @template TRequest - The request options type (query, params, body)
|
|
281
|
+
* @template TMeta - Plugin metadata type
|
|
313
282
|
*/
|
|
314
|
-
type
|
|
283
|
+
type BasePagesOptions<TData, TItem, TError = unknown, TRequest = InfiniteRequestOptions, TMeta = Record<string, unknown>> = {
|
|
315
284
|
/** Whether to fetch automatically on mount. Default: true */
|
|
316
285
|
enabled?: boolean;
|
|
317
286
|
/**
|
|
@@ -322,30 +291,66 @@ type BaseInfiniteReadOptions<TData, TItem, TRequest = AnyInfiniteRequestOptions>
|
|
|
322
291
|
* - 'none' should only be used as string (use `tags: 'none'` not in array)
|
|
323
292
|
*/
|
|
324
293
|
tags?: TagMode | (TagModeInArray | (string & {}))[];
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Callback to determine if there's a next page to fetch.
|
|
296
|
+
* Receives the last page to check for pagination indicators.
|
|
297
|
+
* Default: `() => false` (no next page fetching)
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* canFetchNext: ({ lastPage }) => lastPage?.data?.nextCursor != null
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
canFetchNext?: (ctx: InfiniteNextContext<TData, TError, TRequest, TMeta>) => boolean;
|
|
305
|
+
/**
|
|
306
|
+
* Callback to build the request options for the next page.
|
|
307
|
+
* Return only the fields that change - they will be **merged** with the initial request.
|
|
308
|
+
* Default: `() => ({})` (no changes to request)
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* // Initial: { query: { cursor: 0, limit: 10 } }
|
|
313
|
+
* // Only return cursor - limit is preserved automatically
|
|
314
|
+
* nextPageRequest: ({ lastPage }) => ({
|
|
315
|
+
* query: { cursor: lastPage?.data?.nextCursor }
|
|
316
|
+
* })
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
nextPageRequest?: (ctx: InfiniteNextContext<TData, TError, TRequest, TMeta>) => Partial<TRequest>;
|
|
320
|
+
/**
|
|
321
|
+
* Callback to merge all pages into a single array of items.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
merger: (pages: InfinitePage<TData, TError, TMeta>[]) => TItem[];
|
|
329
|
+
/**
|
|
330
|
+
* Callback to determine if there's a previous page to fetch.
|
|
331
|
+
* Receives the first page to check for pagination indicators.
|
|
332
|
+
*/
|
|
333
|
+
canFetchPrev?: (ctx: InfinitePrevContext<TData, TError, TRequest, TMeta>) => boolean;
|
|
334
|
+
/**
|
|
335
|
+
* Callback to build the request options for the previous page.
|
|
336
|
+
* Return only the fields that change - they will be **merged** with the initial request.
|
|
337
|
+
*/
|
|
338
|
+
prevPageRequest?: (ctx: InfinitePrevContext<TData, TError, TRequest, TMeta>) => Partial<TRequest>;
|
|
335
339
|
};
|
|
336
340
|
/**
|
|
337
|
-
* Result returned by `
|
|
341
|
+
* Result returned by `usePages` hook.
|
|
338
342
|
*
|
|
339
343
|
* @template TData - The response data type for each page
|
|
340
344
|
* @template TError - The error type
|
|
341
345
|
* @template TItem - The item type after merging all responses
|
|
342
346
|
* @template TPluginResult - Plugin-provided result fields
|
|
347
|
+
* @template TTriggerOptions - Options that can be passed to trigger()
|
|
343
348
|
*/
|
|
344
|
-
type
|
|
345
|
-
/** Merged items from all fetched
|
|
349
|
+
type BasePagesResult<TData, TError, TItem, TPluginResult = Record<string, unknown>, TTriggerOptions = object> = {
|
|
350
|
+
/** Merged items from all fetched pages */
|
|
346
351
|
data: TItem[] | undefined;
|
|
347
|
-
/** Array of all
|
|
348
|
-
|
|
352
|
+
/** Array of all pages with status, data, error, and meta per page */
|
|
353
|
+
pages: InfinitePage<TData, TError, TPluginResult>[];
|
|
349
354
|
/** True during the initial load (no data yet) */
|
|
350
355
|
loading: boolean;
|
|
351
356
|
/** True during any fetch operation */
|
|
@@ -358,21 +363,19 @@ type BaseInfiniteReadResult<TData, TError, TItem, TPluginResult = Record<string,
|
|
|
358
363
|
canFetchNext: boolean;
|
|
359
364
|
/** Whether there's a previous page available to fetch */
|
|
360
365
|
canFetchPrev: boolean;
|
|
361
|
-
/** Plugin-provided metadata */
|
|
362
|
-
meta: TPluginResult;
|
|
363
366
|
/** Fetch the next page */
|
|
364
367
|
fetchNext: () => Promise<void>;
|
|
365
368
|
/** Fetch the previous page */
|
|
366
369
|
fetchPrev: () => Promise<void>;
|
|
367
|
-
/** Trigger refetch of all pages from the beginning */
|
|
368
|
-
trigger: () => Promise<void>;
|
|
370
|
+
/** Trigger refetch of all pages from the beginning, optionally with new request options */
|
|
371
|
+
trigger: (options?: TTriggerOptions) => Promise<void>;
|
|
369
372
|
/** Abort the current fetch operation */
|
|
370
373
|
abort: () => void;
|
|
371
374
|
/** Error from the last failed request */
|
|
372
375
|
error: TError | undefined;
|
|
373
376
|
};
|
|
374
|
-
type
|
|
375
|
-
type
|
|
377
|
+
type UsePagesResult<TData, TError, TItem, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TTriggerOptions = object> = BasePagesResult<TData, TError, TItem, MergePluginResults<TPlugins>["read"], TTriggerOptions>;
|
|
378
|
+
type PagesApiClient<TSchema, TDefaultError> = ReadClient<TSchema, TDefaultError>;
|
|
376
379
|
|
|
377
380
|
type InferError<T, TDefaultError> = [T] extends [unknown] ? TDefaultError : T;
|
|
378
381
|
type WriteResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
|
|
@@ -391,17 +394,9 @@ type UseWriteFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
|
|
|
391
394
|
type UseQueueFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
|
|
392
395
|
<TQueueFn extends (api: QueueApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>>(queueFn: TQueueFn, queueOptions?: ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions): UseQueueResult<ExtractMethodData<TQueueFn>, InferError<ExtractMethodError<TQueueFn>, TDefaultError>, QueueTriggerInput<TQueueFn> & ResolvedQueueTriggerOptions<TSchema, TPlugins, TQueueFn, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["queue"], ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions>>;
|
|
393
396
|
};
|
|
394
|
-
type
|
|
395
|
-
query: infer Q;
|
|
396
|
-
} ? Q : never, TRequest extends {
|
|
397
|
-
body: infer B;
|
|
398
|
-
} ? B : never, TRequest extends {
|
|
399
|
-
params: infer P;
|
|
400
|
-
} ? P : never>;
|
|
401
|
-
type ResolvedInfiniteReadOptions<TSchema, TPlugins extends PluginArray, TData, TError, TRequest> = ResolveTypes<MergePluginOptions<TPlugins>["infiniteRead"], InfiniteReadResolverContext<TSchema, TData, TError, TRequest>>;
|
|
402
|
-
type UseInfiniteReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TData, TItem, TError = TDefaultError, TRequest extends AnyInfiniteRequestOptions = AnyInfiniteRequestOptions>(readFn: (api: InfiniteReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<TData, TError>>, readOptions: BaseInfiniteReadOptions<TData, TItem, TRequest> & ResolvedInfiniteReadOptions<TSchema, TPlugins, TData, TError, TRequest>) => BaseInfiniteReadResult<TData, TError, TItem, MergePluginResults<TPlugins>["read"]>;
|
|
397
|
+
type UsePagesFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TReadFn extends (api: PagesApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions, TItem = unknown>(readFn: TReadFn, readOptions: BasePagesOptions<ExtractMethodData<TReadFn>, TItem, InferError<ExtractMethodError<TReadFn>, TDefaultError>, TRequest, MergePluginResults<TPlugins>["read"]> & ResolveTypes<MergePluginOptions<TPlugins>["pages"], ResolverContext<TSchema, ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>>>) => BasePagesResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, TItem, MergePluginResults<TPlugins>["read"], PagesTriggerOptions<TReadFn>>;
|
|
403
398
|
/**
|
|
404
|
-
* Spoosh React hooks interface containing useRead, useWrite, and
|
|
399
|
+
* Spoosh React hooks interface containing useRead, useWrite, and usePages.
|
|
405
400
|
*
|
|
406
401
|
* @template TDefaultError - The default error type
|
|
407
402
|
* @template TSchema - The API schema type
|
|
@@ -448,21 +443,21 @@ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
|
|
|
448
443
|
*
|
|
449
444
|
* @param readFn - Function that selects the API endpoint to call
|
|
450
445
|
* @param readOptions - Configuration including `canFetchNext`, `nextPageRequest`, `merger`, and optional `canFetchPrev`/`prevPageRequest`
|
|
451
|
-
* @returns Object containing `data`, `
|
|
446
|
+
* @returns Object containing `data`, `pages`, `fetchNext`, `fetchPrev`, `canFetchNext`, `canFetchPrev`, `loading`, `fetching`, and pagination states
|
|
452
447
|
*
|
|
453
448
|
* @example
|
|
454
449
|
* ```tsx
|
|
455
|
-
* const { data, fetchNext, canFetchNext, loading } =
|
|
450
|
+
* const { data, fetchNext, canFetchNext, loading } = usePages(
|
|
456
451
|
* (api) => api("posts").GET(),
|
|
457
452
|
* {
|
|
458
|
-
* canFetchNext: ({
|
|
459
|
-
* nextPageRequest: ({
|
|
460
|
-
* merger: (
|
|
453
|
+
* canFetchNext: ({ lastPage }) => !!lastPage?.data?.nextCursor,
|
|
454
|
+
* nextPageRequest: ({ lastPage }) => ({ query: { cursor: lastPage?.data?.nextCursor } }),
|
|
455
|
+
* merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
|
|
461
456
|
* }
|
|
462
457
|
* );
|
|
463
458
|
* ```
|
|
464
459
|
*/
|
|
465
|
-
|
|
460
|
+
usePages: UsePagesFn<TDefaultError, TSchema, TPlugins>;
|
|
466
461
|
/**
|
|
467
462
|
* React hook for queued operations with concurrency control.
|
|
468
463
|
*
|
|
@@ -505,18 +500,18 @@ type SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins> = {
|
|
|
505
500
|
};
|
|
506
501
|
|
|
507
502
|
/**
|
|
508
|
-
* Creates React hooks (useRead, useWrite,
|
|
503
|
+
* Creates React hooks (useRead, useWrite, usePages) from a Spoosh instance.
|
|
509
504
|
*
|
|
510
505
|
* @template TSchema - The API schema type
|
|
511
506
|
* @template TDefaultError - The default error type
|
|
512
507
|
* @template TPlugins - The plugins array type
|
|
513
508
|
* @template TApi - The API type
|
|
514
509
|
* @param instance - The Spoosh instance containing api, stateManager, eventEmitter, and pluginExecutor
|
|
515
|
-
* @returns An object containing useRead, useWrite,
|
|
510
|
+
* @returns An object containing useRead, useWrite, usePages hooks and plugin instance APIs
|
|
516
511
|
*
|
|
517
512
|
* @example
|
|
518
513
|
* ```ts
|
|
519
|
-
* const { useRead, useWrite,
|
|
514
|
+
* const { useRead, useWrite, usePages } = create(spooshInstance);
|
|
520
515
|
*
|
|
521
516
|
* function MyComponent() {
|
|
522
517
|
* const { data, loading } = useRead((api) => api("posts").GET());
|
|
@@ -532,4 +527,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
|
|
|
532
527
|
plugins: TPlugins;
|
|
533
528
|
};
|
|
534
529
|
|
|
535
|
-
export { type
|
|
530
|
+
export { type BasePagesOptions, type BasePagesResult, type BaseReadOptions, type BaseReadResult, type BaseWriteResult, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
|
package/dist/index.js
CHANGED
|
@@ -404,12 +404,12 @@ function createUseWrite(options) {
|
|
|
404
404
|
return useWrite;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
// src/
|
|
407
|
+
// src/usePages/index.ts
|
|
408
408
|
var import_react3 = require("react");
|
|
409
409
|
var import_core3 = require("@spoosh/core");
|
|
410
|
-
function
|
|
410
|
+
function createUsePages(options) {
|
|
411
411
|
const { api, stateManager, eventEmitter, pluginExecutor } = options;
|
|
412
|
-
return function
|
|
412
|
+
return function usePages(readFn, readOptions) {
|
|
413
413
|
const {
|
|
414
414
|
enabled = true,
|
|
415
415
|
tags,
|
|
@@ -432,7 +432,7 @@ function createUseInfiniteRead(options) {
|
|
|
432
432
|
const capturedCall = selectorResultRef.current.call;
|
|
433
433
|
if (!capturedCall) {
|
|
434
434
|
throw new Error(
|
|
435
|
-
'
|
|
435
|
+
'usePages requires calling an HTTP method (GET). Example: usePages((api) => api("posts").GET())'
|
|
436
436
|
);
|
|
437
437
|
}
|
|
438
438
|
const requestOptions = capturedCall.options;
|
|
@@ -442,12 +442,6 @@ function createUseInfiniteRead(options) {
|
|
|
442
442
|
params: requestOptions?.params,
|
|
443
443
|
body: requestOptions?.body
|
|
444
444
|
};
|
|
445
|
-
const baseOptionsForKey = {
|
|
446
|
-
...capturedCall.options,
|
|
447
|
-
query: void 0,
|
|
448
|
-
params: void 0,
|
|
449
|
-
body: void 0
|
|
450
|
-
};
|
|
451
445
|
const resolvedPath = (0, import_core3.resolvePath)(pathSegments, requestOptions?.params);
|
|
452
446
|
const resolvedTags = (0, import_core3.resolveTags)({ tags }, resolvedPath);
|
|
453
447
|
const canFetchNextRef = (0, import_react3.useRef)(canFetchNext);
|
|
@@ -463,22 +457,31 @@ function createUseInfiniteRead(options) {
|
|
|
463
457
|
const queryKey = stateManager.createQueryKey({
|
|
464
458
|
path: capturedCall.path,
|
|
465
459
|
method: capturedCall.method,
|
|
466
|
-
options:
|
|
460
|
+
options: capturedCall.options
|
|
461
|
+
});
|
|
462
|
+
const lifecycleRef = (0, import_react3.useRef)({
|
|
463
|
+
initialized: false,
|
|
464
|
+
prevContext: null,
|
|
465
|
+
lastQueryKey: null
|
|
467
466
|
});
|
|
468
467
|
const controllerRef = (0, import_react3.useRef)(null);
|
|
469
|
-
|
|
468
|
+
const queryKeyChanged = controllerRef.current !== null && controllerRef.current.queryKey !== queryKey;
|
|
469
|
+
if (queryKeyChanged) {
|
|
470
|
+
lifecycleRef.current.prevContext = controllerRef.current.controller.getContext();
|
|
471
|
+
lifecycleRef.current.initialized = false;
|
|
472
|
+
}
|
|
473
|
+
if (!controllerRef.current || queryKeyChanged) {
|
|
470
474
|
controllerRef.current = {
|
|
471
475
|
controller: (0, import_core3.createInfiniteReadController)({
|
|
472
476
|
path: capturedCall.path,
|
|
473
477
|
method: capturedCall.method,
|
|
474
478
|
tags: resolvedTags,
|
|
475
479
|
initialRequest,
|
|
476
|
-
|
|
477
|
-
canFetchNext: (ctx) => canFetchNextRef.current(ctx),
|
|
480
|
+
canFetchNext: canFetchNext ? (ctx) => canFetchNextRef.current?.(ctx) ?? false : void 0,
|
|
478
481
|
canFetchPrev: canFetchPrev ? (ctx) => canFetchPrevRef.current?.(ctx) ?? false : void 0,
|
|
479
|
-
nextPageRequest: (ctx) => nextPageRequestRef.current(ctx),
|
|
482
|
+
nextPageRequest: nextPageRequest ? (ctx) => nextPageRequestRef.current?.(ctx) ?? {} : void 0,
|
|
480
483
|
prevPageRequest: prevPageRequest ? (ctx) => prevPageRequestRef.current?.(ctx) ?? {} : void 0,
|
|
481
|
-
merger: (
|
|
484
|
+
merger: (pages) => mergerRef.current(pages),
|
|
482
485
|
stateManager,
|
|
483
486
|
eventEmitter,
|
|
484
487
|
pluginExecutor,
|
|
@@ -488,9 +491,7 @@ function createUseInfiniteRead(options) {
|
|
|
488
491
|
const method = pathMethods[capturedCall.method];
|
|
489
492
|
const fetchOptions = {
|
|
490
493
|
...capturedCall.options,
|
|
491
|
-
|
|
492
|
-
params: opts.params,
|
|
493
|
-
body: opts.body,
|
|
494
|
+
...opts,
|
|
494
495
|
signal
|
|
495
496
|
};
|
|
496
497
|
return method(fetchOptions);
|
|
@@ -501,11 +502,12 @@ function createUseInfiniteRead(options) {
|
|
|
501
502
|
}
|
|
502
503
|
const controller = controllerRef.current.controller;
|
|
503
504
|
controller.setPluginOptions(pluginOpts);
|
|
504
|
-
const
|
|
505
|
-
controller.subscribe,
|
|
506
|
-
controller
|
|
507
|
-
controller.getState
|
|
505
|
+
const subscribe = (0, import_react3.useCallback)(
|
|
506
|
+
(callback) => controller.subscribe(callback),
|
|
507
|
+
[controller]
|
|
508
508
|
);
|
|
509
|
+
const getSnapshot = (0, import_react3.useCallback)(() => controller.getState(), [controller]);
|
|
510
|
+
const state = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
509
511
|
const [isPending, setIsPending] = (0, import_react3.useState)(() => {
|
|
510
512
|
return enabled && state.data === void 0;
|
|
511
513
|
});
|
|
@@ -515,10 +517,6 @@ function createUseInfiniteRead(options) {
|
|
|
515
517
|
const fetchingPrev = fetchingDirection === "prev";
|
|
516
518
|
const hasData = state.data !== void 0;
|
|
517
519
|
const loading = (isPending || fetching) && !hasData;
|
|
518
|
-
const lifecycleRef = (0, import_react3.useRef)({
|
|
519
|
-
initialized: false,
|
|
520
|
-
prevContext: null
|
|
521
|
-
});
|
|
522
520
|
const tagsKey = JSON.stringify(tags);
|
|
523
521
|
(0, import_react3.useEffect)(() => {
|
|
524
522
|
return () => {
|
|
@@ -527,8 +525,27 @@ function createUseInfiniteRead(options) {
|
|
|
527
525
|
};
|
|
528
526
|
}, []);
|
|
529
527
|
(0, import_react3.useEffect)(() => {
|
|
530
|
-
|
|
531
|
-
|
|
528
|
+
if (!enabled) return;
|
|
529
|
+
const { initialized, prevContext, lastQueryKey } = lifecycleRef.current;
|
|
530
|
+
const isQueryKeyChange = lastQueryKey !== null && lastQueryKey !== queryKey;
|
|
531
|
+
if (!initialized) {
|
|
532
|
+
controller.mount();
|
|
533
|
+
lifecycleRef.current.initialized = true;
|
|
534
|
+
if (prevContext) {
|
|
535
|
+
controller.update(prevContext);
|
|
536
|
+
lifecycleRef.current.prevContext = null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
lifecycleRef.current.lastQueryKey = queryKey;
|
|
540
|
+
const currentState = controller.getState();
|
|
541
|
+
const isFetching = controller.getFetchingDirection() !== null;
|
|
542
|
+
if (isQueryKeyChange) {
|
|
543
|
+
setIsPending(true);
|
|
544
|
+
controller.trigger({ force: false }).finally(() => setIsPending(false));
|
|
545
|
+
} else if (currentState.data === void 0 && !isFetching) {
|
|
546
|
+
setIsPending(true);
|
|
547
|
+
controller.fetchNext().finally(() => setIsPending(false));
|
|
548
|
+
}
|
|
532
549
|
const unsubInvalidate = eventEmitter.on(
|
|
533
550
|
"invalidate",
|
|
534
551
|
(invalidatedTags) => {
|
|
@@ -537,41 +554,32 @@ function createUseInfiniteRead(options) {
|
|
|
537
554
|
);
|
|
538
555
|
if (hasMatch) {
|
|
539
556
|
setIsPending(true);
|
|
540
|
-
controller.
|
|
557
|
+
controller.trigger().finally(() => setIsPending(false));
|
|
541
558
|
}
|
|
542
559
|
}
|
|
543
560
|
);
|
|
544
561
|
const unsubRefetchAll = eventEmitter.on("refetchAll", () => {
|
|
545
562
|
setIsPending(true);
|
|
546
|
-
controller.
|
|
563
|
+
controller.trigger().finally(() => setIsPending(false));
|
|
547
564
|
});
|
|
548
565
|
return () => {
|
|
566
|
+
controller.unmount();
|
|
549
567
|
unsubInvalidate();
|
|
550
568
|
unsubRefetchAll();
|
|
551
569
|
};
|
|
552
|
-
}, [tagsKey]);
|
|
553
|
-
|
|
554
|
-
if (!lifecycleRef.current.initialized) return;
|
|
555
|
-
if (enabled) {
|
|
556
|
-
const currentState = controller.getState();
|
|
557
|
-
const isFetching = controller.getFetchingDirection() !== null;
|
|
558
|
-
if (currentState.data === void 0 && !isFetching) {
|
|
559
|
-
setIsPending(true);
|
|
560
|
-
controller.fetchNext().finally(() => setIsPending(false));
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}, [enabled]);
|
|
570
|
+
}, [queryKey, enabled, tagsKey]);
|
|
571
|
+
const pluginOptsKey = JSON.stringify(pluginOpts);
|
|
564
572
|
(0, import_react3.useEffect)(() => {
|
|
565
573
|
if (!enabled || !lifecycleRef.current.initialized) return;
|
|
566
574
|
const prevContext = controller.getContext();
|
|
567
575
|
controller.update(prevContext);
|
|
568
|
-
}, [
|
|
569
|
-
const
|
|
570
|
-
|
|
576
|
+
}, [pluginOptsKey]);
|
|
577
|
+
const trigger = async (options2) => {
|
|
578
|
+
await controller.trigger(options2);
|
|
579
|
+
};
|
|
571
580
|
const result = {
|
|
572
|
-
meta: pluginResultData,
|
|
573
581
|
data: state.data,
|
|
574
|
-
|
|
582
|
+
pages: state.pages,
|
|
575
583
|
loading,
|
|
576
584
|
fetching,
|
|
577
585
|
fetchingNext,
|
|
@@ -580,7 +588,7 @@ function createUseInfiniteRead(options) {
|
|
|
580
588
|
canFetchPrev: state.canFetchPrev,
|
|
581
589
|
fetchNext: controller.fetchNext,
|
|
582
590
|
fetchPrev: controller.fetchPrev,
|
|
583
|
-
trigger
|
|
591
|
+
trigger,
|
|
584
592
|
abort: controller.abort,
|
|
585
593
|
error: state.error
|
|
586
594
|
};
|
|
@@ -669,7 +677,7 @@ function create(instance) {
|
|
|
669
677
|
eventEmitter,
|
|
670
678
|
pluginExecutor
|
|
671
679
|
});
|
|
672
|
-
const
|
|
680
|
+
const usePages = createUsePages({
|
|
673
681
|
api,
|
|
674
682
|
stateManager,
|
|
675
683
|
eventEmitter,
|
|
@@ -708,7 +716,7 @@ function create(instance) {
|
|
|
708
716
|
return {
|
|
709
717
|
useRead,
|
|
710
718
|
useWrite,
|
|
711
|
-
|
|
719
|
+
usePages,
|
|
712
720
|
useQueue,
|
|
713
721
|
...instanceApis
|
|
714
722
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -402,13 +402,14 @@ function createUseWrite(options) {
|
|
|
402
402
|
return useWrite;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
// src/
|
|
405
|
+
// src/usePages/index.ts
|
|
406
406
|
import {
|
|
407
407
|
useRef as useRef3,
|
|
408
408
|
useEffect as useEffect2,
|
|
409
409
|
useSyncExternalStore as useSyncExternalStore3,
|
|
410
410
|
useId as useId3,
|
|
411
|
-
useState as useState3
|
|
411
|
+
useState as useState3,
|
|
412
|
+
useCallback as useCallback3
|
|
412
413
|
} from "react";
|
|
413
414
|
import {
|
|
414
415
|
createInfiniteReadController,
|
|
@@ -416,9 +417,9 @@ import {
|
|
|
416
417
|
resolvePath as resolvePath3,
|
|
417
418
|
resolveTags as resolveTags3
|
|
418
419
|
} from "@spoosh/core";
|
|
419
|
-
function
|
|
420
|
+
function createUsePages(options) {
|
|
420
421
|
const { api, stateManager, eventEmitter, pluginExecutor } = options;
|
|
421
|
-
return function
|
|
422
|
+
return function usePages(readFn, readOptions) {
|
|
422
423
|
const {
|
|
423
424
|
enabled = true,
|
|
424
425
|
tags,
|
|
@@ -441,7 +442,7 @@ function createUseInfiniteRead(options) {
|
|
|
441
442
|
const capturedCall = selectorResultRef.current.call;
|
|
442
443
|
if (!capturedCall) {
|
|
443
444
|
throw new Error(
|
|
444
|
-
'
|
|
445
|
+
'usePages requires calling an HTTP method (GET). Example: usePages((api) => api("posts").GET())'
|
|
445
446
|
);
|
|
446
447
|
}
|
|
447
448
|
const requestOptions = capturedCall.options;
|
|
@@ -451,12 +452,6 @@ function createUseInfiniteRead(options) {
|
|
|
451
452
|
params: requestOptions?.params,
|
|
452
453
|
body: requestOptions?.body
|
|
453
454
|
};
|
|
454
|
-
const baseOptionsForKey = {
|
|
455
|
-
...capturedCall.options,
|
|
456
|
-
query: void 0,
|
|
457
|
-
params: void 0,
|
|
458
|
-
body: void 0
|
|
459
|
-
};
|
|
460
455
|
const resolvedPath = resolvePath3(pathSegments, requestOptions?.params);
|
|
461
456
|
const resolvedTags = resolveTags3({ tags }, resolvedPath);
|
|
462
457
|
const canFetchNextRef = useRef3(canFetchNext);
|
|
@@ -472,22 +467,31 @@ function createUseInfiniteRead(options) {
|
|
|
472
467
|
const queryKey = stateManager.createQueryKey({
|
|
473
468
|
path: capturedCall.path,
|
|
474
469
|
method: capturedCall.method,
|
|
475
|
-
options:
|
|
470
|
+
options: capturedCall.options
|
|
471
|
+
});
|
|
472
|
+
const lifecycleRef = useRef3({
|
|
473
|
+
initialized: false,
|
|
474
|
+
prevContext: null,
|
|
475
|
+
lastQueryKey: null
|
|
476
476
|
});
|
|
477
477
|
const controllerRef = useRef3(null);
|
|
478
|
-
|
|
478
|
+
const queryKeyChanged = controllerRef.current !== null && controllerRef.current.queryKey !== queryKey;
|
|
479
|
+
if (queryKeyChanged) {
|
|
480
|
+
lifecycleRef.current.prevContext = controllerRef.current.controller.getContext();
|
|
481
|
+
lifecycleRef.current.initialized = false;
|
|
482
|
+
}
|
|
483
|
+
if (!controllerRef.current || queryKeyChanged) {
|
|
479
484
|
controllerRef.current = {
|
|
480
485
|
controller: createInfiniteReadController({
|
|
481
486
|
path: capturedCall.path,
|
|
482
487
|
method: capturedCall.method,
|
|
483
488
|
tags: resolvedTags,
|
|
484
489
|
initialRequest,
|
|
485
|
-
|
|
486
|
-
canFetchNext: (ctx) => canFetchNextRef.current(ctx),
|
|
490
|
+
canFetchNext: canFetchNext ? (ctx) => canFetchNextRef.current?.(ctx) ?? false : void 0,
|
|
487
491
|
canFetchPrev: canFetchPrev ? (ctx) => canFetchPrevRef.current?.(ctx) ?? false : void 0,
|
|
488
|
-
nextPageRequest: (ctx) => nextPageRequestRef.current(ctx),
|
|
492
|
+
nextPageRequest: nextPageRequest ? (ctx) => nextPageRequestRef.current?.(ctx) ?? {} : void 0,
|
|
489
493
|
prevPageRequest: prevPageRequest ? (ctx) => prevPageRequestRef.current?.(ctx) ?? {} : void 0,
|
|
490
|
-
merger: (
|
|
494
|
+
merger: (pages) => mergerRef.current(pages),
|
|
491
495
|
stateManager,
|
|
492
496
|
eventEmitter,
|
|
493
497
|
pluginExecutor,
|
|
@@ -497,9 +501,7 @@ function createUseInfiniteRead(options) {
|
|
|
497
501
|
const method = pathMethods[capturedCall.method];
|
|
498
502
|
const fetchOptions = {
|
|
499
503
|
...capturedCall.options,
|
|
500
|
-
|
|
501
|
-
params: opts.params,
|
|
502
|
-
body: opts.body,
|
|
504
|
+
...opts,
|
|
503
505
|
signal
|
|
504
506
|
};
|
|
505
507
|
return method(fetchOptions);
|
|
@@ -510,11 +512,12 @@ function createUseInfiniteRead(options) {
|
|
|
510
512
|
}
|
|
511
513
|
const controller = controllerRef.current.controller;
|
|
512
514
|
controller.setPluginOptions(pluginOpts);
|
|
513
|
-
const
|
|
514
|
-
controller.subscribe,
|
|
515
|
-
controller
|
|
516
|
-
controller.getState
|
|
515
|
+
const subscribe = useCallback3(
|
|
516
|
+
(callback) => controller.subscribe(callback),
|
|
517
|
+
[controller]
|
|
517
518
|
);
|
|
519
|
+
const getSnapshot = useCallback3(() => controller.getState(), [controller]);
|
|
520
|
+
const state = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
|
|
518
521
|
const [isPending, setIsPending] = useState3(() => {
|
|
519
522
|
return enabled && state.data === void 0;
|
|
520
523
|
});
|
|
@@ -524,10 +527,6 @@ function createUseInfiniteRead(options) {
|
|
|
524
527
|
const fetchingPrev = fetchingDirection === "prev";
|
|
525
528
|
const hasData = state.data !== void 0;
|
|
526
529
|
const loading = (isPending || fetching) && !hasData;
|
|
527
|
-
const lifecycleRef = useRef3({
|
|
528
|
-
initialized: false,
|
|
529
|
-
prevContext: null
|
|
530
|
-
});
|
|
531
530
|
const tagsKey = JSON.stringify(tags);
|
|
532
531
|
useEffect2(() => {
|
|
533
532
|
return () => {
|
|
@@ -536,8 +535,27 @@ function createUseInfiniteRead(options) {
|
|
|
536
535
|
};
|
|
537
536
|
}, []);
|
|
538
537
|
useEffect2(() => {
|
|
539
|
-
|
|
540
|
-
|
|
538
|
+
if (!enabled) return;
|
|
539
|
+
const { initialized, prevContext, lastQueryKey } = lifecycleRef.current;
|
|
540
|
+
const isQueryKeyChange = lastQueryKey !== null && lastQueryKey !== queryKey;
|
|
541
|
+
if (!initialized) {
|
|
542
|
+
controller.mount();
|
|
543
|
+
lifecycleRef.current.initialized = true;
|
|
544
|
+
if (prevContext) {
|
|
545
|
+
controller.update(prevContext);
|
|
546
|
+
lifecycleRef.current.prevContext = null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
lifecycleRef.current.lastQueryKey = queryKey;
|
|
550
|
+
const currentState = controller.getState();
|
|
551
|
+
const isFetching = controller.getFetchingDirection() !== null;
|
|
552
|
+
if (isQueryKeyChange) {
|
|
553
|
+
setIsPending(true);
|
|
554
|
+
controller.trigger({ force: false }).finally(() => setIsPending(false));
|
|
555
|
+
} else if (currentState.data === void 0 && !isFetching) {
|
|
556
|
+
setIsPending(true);
|
|
557
|
+
controller.fetchNext().finally(() => setIsPending(false));
|
|
558
|
+
}
|
|
541
559
|
const unsubInvalidate = eventEmitter.on(
|
|
542
560
|
"invalidate",
|
|
543
561
|
(invalidatedTags) => {
|
|
@@ -546,41 +564,32 @@ function createUseInfiniteRead(options) {
|
|
|
546
564
|
);
|
|
547
565
|
if (hasMatch) {
|
|
548
566
|
setIsPending(true);
|
|
549
|
-
controller.
|
|
567
|
+
controller.trigger().finally(() => setIsPending(false));
|
|
550
568
|
}
|
|
551
569
|
}
|
|
552
570
|
);
|
|
553
571
|
const unsubRefetchAll = eventEmitter.on("refetchAll", () => {
|
|
554
572
|
setIsPending(true);
|
|
555
|
-
controller.
|
|
573
|
+
controller.trigger().finally(() => setIsPending(false));
|
|
556
574
|
});
|
|
557
575
|
return () => {
|
|
576
|
+
controller.unmount();
|
|
558
577
|
unsubInvalidate();
|
|
559
578
|
unsubRefetchAll();
|
|
560
579
|
};
|
|
561
|
-
}, [tagsKey]);
|
|
562
|
-
|
|
563
|
-
if (!lifecycleRef.current.initialized) return;
|
|
564
|
-
if (enabled) {
|
|
565
|
-
const currentState = controller.getState();
|
|
566
|
-
const isFetching = controller.getFetchingDirection() !== null;
|
|
567
|
-
if (currentState.data === void 0 && !isFetching) {
|
|
568
|
-
setIsPending(true);
|
|
569
|
-
controller.fetchNext().finally(() => setIsPending(false));
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}, [enabled]);
|
|
580
|
+
}, [queryKey, enabled, tagsKey]);
|
|
581
|
+
const pluginOptsKey = JSON.stringify(pluginOpts);
|
|
573
582
|
useEffect2(() => {
|
|
574
583
|
if (!enabled || !lifecycleRef.current.initialized) return;
|
|
575
584
|
const prevContext = controller.getContext();
|
|
576
585
|
controller.update(prevContext);
|
|
577
|
-
}, [
|
|
578
|
-
const
|
|
579
|
-
|
|
586
|
+
}, [pluginOptsKey]);
|
|
587
|
+
const trigger = async (options2) => {
|
|
588
|
+
await controller.trigger(options2);
|
|
589
|
+
};
|
|
580
590
|
const result = {
|
|
581
|
-
meta: pluginResultData,
|
|
582
591
|
data: state.data,
|
|
583
|
-
|
|
592
|
+
pages: state.pages,
|
|
584
593
|
loading,
|
|
585
594
|
fetching,
|
|
586
595
|
fetchingNext,
|
|
@@ -589,7 +598,7 @@ function createUseInfiniteRead(options) {
|
|
|
589
598
|
canFetchPrev: state.canFetchPrev,
|
|
590
599
|
fetchNext: controller.fetchNext,
|
|
591
600
|
fetchPrev: controller.fetchPrev,
|
|
592
|
-
trigger
|
|
601
|
+
trigger,
|
|
593
602
|
abort: controller.abort,
|
|
594
603
|
error: state.error
|
|
595
604
|
};
|
|
@@ -681,7 +690,7 @@ function create(instance) {
|
|
|
681
690
|
eventEmitter,
|
|
682
691
|
pluginExecutor
|
|
683
692
|
});
|
|
684
|
-
const
|
|
693
|
+
const usePages = createUsePages({
|
|
685
694
|
api,
|
|
686
695
|
stateManager,
|
|
687
696
|
eventEmitter,
|
|
@@ -720,7 +729,7 @@ function create(instance) {
|
|
|
720
729
|
return {
|
|
721
730
|
useRead,
|
|
722
731
|
useWrite,
|
|
723
|
-
|
|
732
|
+
usePages,
|
|
724
733
|
useQueue,
|
|
725
734
|
...instanceApis
|
|
726
735
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spoosh/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React hooks for Spoosh API toolkit",
|
|
6
6
|
"keywords": [
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@spoosh/core": ">=0.
|
|
37
|
+
"@spoosh/core": ">=0.15.0",
|
|
38
38
|
"react": "^18 || ^19"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@testing-library/react": "^16.0.0",
|
|
42
42
|
"jsdom": "^26.0.0",
|
|
43
|
-
"@spoosh/core": "0.
|
|
44
|
-
"@spoosh/test-utils": "0.
|
|
43
|
+
"@spoosh/core": "0.15.0",
|
|
44
|
+
"@spoosh/test-utils": "0.3.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"dev": "tsup --watch",
|