@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @spoosh/react
2
2
 
3
- React hooks for Spoosh - `useRead`, `useWrite`, and `useInfiniteRead`.
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, useInfiniteRead } = create(spoosh);
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
- ### useInfiniteRead
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
- allResponses,
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
- } = useInfiniteRead(
117
+ } = usePages(
118
118
  (api) => api("posts").GET({ query: { page: 1 } }),
119
119
  {
120
120
  // Required: Check if next page exists
121
- canFetchNext: ({ response }) => response?.meta.hasMore ?? false,
121
+ canFetchNext: ({ lastPage }) => lastPage?.data?.meta.hasMore ?? false,
122
122
 
123
123
  // Required: Build request for next page
124
- nextPageRequest: ({ response, request }) => ({
125
- query: { ...request.query, page: (response?.meta.page ?? 0) + 1 },
124
+ nextPageRequest: ({ lastPage }) => ({
125
+ query: { page: (lastPage?.data?.meta.page ?? 0) + 1 },
126
126
  }),
127
127
 
128
- // Required: Merge all responses into items
129
- merger: (allResponses) => allResponses.flatMap((r) => r.items),
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: ({ response }) => (response?.meta.page ?? 1) > 1,
132
+ canFetchPrev: ({ firstPage }) => (firstPage?.data?.meta.page ?? 1) > 1,
133
133
 
134
134
  // Optional: Build request for previous page
135
- prevPageRequest: ({ response, request }) => ({
136
- query: { ...request.query, page: (response?.meta.page ?? 2) - 1 },
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
- ### useInfiniteRead(readFn, options)
195
+ ### usePages(readFn, options)
196
196
 
197
- | Option | Type | Required | Description |
198
- | ----------------- | ---------------------------- | -------- | ------------------------------- |
199
- | `canFetchNext` | `(ctx) => boolean` | Yes | Check if next page exists |
200
- | `nextPageRequest` | `(ctx) => Partial<TRequest>` | Yes | Build request for next page |
201
- | `merger` | `(allResponses) => TItem[]` | Yes | Merge all responses into items |
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
- type Context<TData, TRequest> = {
210
- response: TData | undefined; // Latest response
211
- allResponses: TData[]; // All fetched responses
212
- request: TRequest; // Current request options
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 | Description |
219
- | -------------- | ---------------------- | ------------------------------- |
220
- | `data` | `TItem[] \| undefined` | Merged items from all responses |
221
- | `allResponses` | `TData[] \| undefined` | Array of all raw responses |
222
- | `loading` | `boolean` | True during initial load |
223
- | `fetching` | `boolean` | True during any fetch |
224
- | `fetchingNext` | `boolean` | True while fetching next page |
225
- | `fetchingPrev` | `boolean` | True while fetching previous |
226
- | `canFetchNext` | `boolean` | Whether next page exists |
227
- | `canFetchPrev` | `boolean` | Whether previous page exists |
228
- | `fetchNext` | `() => Promise<void>` | Fetch the next page |
229
- | `fetchPrev` | `() => Promise<void>` | Fetch the previous page |
230
- | `refetch` | `() => Promise<void>` | Refetch all pages |
231
- | `abort` | `() => void` | Abort current request |
232
- | `error` | `TError \| undefined` | Error if request failed |
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$2<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
- type ExtractInputFromResponse$2<T> = T extends {
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 ExtractTriggerQuery$2<I> = I extends {
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$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
- type ExtractInputFromResponse$1<T> = T extends {
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$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : object;
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 AnyInfiniteRequestOptions = {
281
- query?: Record<string, unknown>;
282
- params?: Record<string, string | number>;
283
- body?: unknown;
284
- };
285
- /**
286
- * Context passed to `canFetchNext` and `nextPageRequest` callbacks.
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 `useInfiniteRead` hook.
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 BaseInfiniteReadOptions<TData, TItem, TRequest = AnyInfiniteRequestOptions> = {
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
- /** Callback to determine if there's a next page to fetch */
326
- canFetchNext: (ctx: InfiniteNextContext<TData, TRequest>) => boolean;
327
- /** Callback to build the request options for the next page */
328
- nextPageRequest: (ctx: InfiniteNextContext<TData, TRequest>) => Partial<TRequest>;
329
- /** Callback to merge all responses into a single array of items */
330
- merger: (allResponses: TData[]) => TItem[];
331
- /** Callback to determine if there's a previous page to fetch */
332
- canFetchPrev?: (ctx: InfinitePrevContext<TData, TRequest>) => boolean;
333
- /** Callback to build the request options for the previous page */
334
- prevPageRequest?: (ctx: InfinitePrevContext<TData, TRequest>) => Partial<TRequest>;
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 `useInfiniteRead` hook.
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 BaseInfiniteReadResult<TData, TError, TItem, TPluginResult = Record<string, unknown>> = {
345
- /** Merged items from all fetched responses */
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 raw response data */
348
- allResponses: TData[] | undefined;
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 UseInfiniteReadResult<TData, TError, TItem, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseInfiniteReadResult<TData, TError, TItem> & MergePluginResults<TPlugins>["read"];
375
- type InfiniteReadApiClient<TSchema, TDefaultError> = ReadClient<TSchema, TDefaultError>;
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 InfiniteReadResolverContext<TSchema, TData, TError, TRequest> = ResolverContext<TSchema, TData, TError, TRequest extends {
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 useInfiniteRead.
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`, `allResponses`, `fetchNext`, `fetchPrev`, `canFetchNext`, `canFetchPrev`, `loading`, `fetching`, and pagination states
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 } = useInfiniteRead(
450
+ * const { data, fetchNext, canFetchNext, loading } = usePages(
456
451
  * (api) => api("posts").GET(),
457
452
  * {
458
- * canFetchNext: ({ response }) => !!response?.nextCursor,
459
- * nextPageRequest: ({ response }) => ({ query: { cursor: response?.nextCursor } }),
460
- * merger: (responses) => responses.flatMap(r => r.items)
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
- useInfiniteRead: UseInfiniteReadFn<TDefaultError, TSchema, TPlugins>;
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, useInfiniteRead) from a Spoosh instance.
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, useInfiniteRead hooks and plugin instance APIs
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, useInfiniteRead } = create(spooshInstance);
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 AnyInfiniteRequestOptions, type BaseInfiniteReadOptions, type BaseInfiniteReadResult, 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 InfiniteNextContext, type InfinitePrevContext, type InfiniteReadApiClient, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UseInfiniteReadResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
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$2<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
- type ExtractInputFromResponse$2<T> = T extends {
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 ExtractTriggerQuery$2<I> = I extends {
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$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
- type ExtractInputFromResponse$1<T> = T extends {
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$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : object;
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 AnyInfiniteRequestOptions = {
281
- query?: Record<string, unknown>;
282
- params?: Record<string, string | number>;
283
- body?: unknown;
284
- };
285
- /**
286
- * Context passed to `canFetchNext` and `nextPageRequest` callbacks.
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 `useInfiniteRead` hook.
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 BaseInfiniteReadOptions<TData, TItem, TRequest = AnyInfiniteRequestOptions> = {
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
- /** Callback to determine if there's a next page to fetch */
326
- canFetchNext: (ctx: InfiniteNextContext<TData, TRequest>) => boolean;
327
- /** Callback to build the request options for the next page */
328
- nextPageRequest: (ctx: InfiniteNextContext<TData, TRequest>) => Partial<TRequest>;
329
- /** Callback to merge all responses into a single array of items */
330
- merger: (allResponses: TData[]) => TItem[];
331
- /** Callback to determine if there's a previous page to fetch */
332
- canFetchPrev?: (ctx: InfinitePrevContext<TData, TRequest>) => boolean;
333
- /** Callback to build the request options for the previous page */
334
- prevPageRequest?: (ctx: InfinitePrevContext<TData, TRequest>) => Partial<TRequest>;
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 `useInfiniteRead` hook.
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 BaseInfiniteReadResult<TData, TError, TItem, TPluginResult = Record<string, unknown>> = {
345
- /** Merged items from all fetched responses */
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 raw response data */
348
- allResponses: TData[] | undefined;
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 UseInfiniteReadResult<TData, TError, TItem, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseInfiniteReadResult<TData, TError, TItem> & MergePluginResults<TPlugins>["read"];
375
- type InfiniteReadApiClient<TSchema, TDefaultError> = ReadClient<TSchema, TDefaultError>;
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 InfiniteReadResolverContext<TSchema, TData, TError, TRequest> = ResolverContext<TSchema, TData, TError, TRequest extends {
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 useInfiniteRead.
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`, `allResponses`, `fetchNext`, `fetchPrev`, `canFetchNext`, `canFetchPrev`, `loading`, `fetching`, and pagination states
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 } = useInfiniteRead(
450
+ * const { data, fetchNext, canFetchNext, loading } = usePages(
456
451
  * (api) => api("posts").GET(),
457
452
  * {
458
- * canFetchNext: ({ response }) => !!response?.nextCursor,
459
- * nextPageRequest: ({ response }) => ({ query: { cursor: response?.nextCursor } }),
460
- * merger: (responses) => responses.flatMap(r => r.items)
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
- useInfiniteRead: UseInfiniteReadFn<TDefaultError, TSchema, TPlugins>;
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, useInfiniteRead) from a Spoosh instance.
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, useInfiniteRead hooks and plugin instance APIs
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, useInfiniteRead } = create(spooshInstance);
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 AnyInfiniteRequestOptions, type BaseInfiniteReadOptions, type BaseInfiniteReadResult, 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 InfiniteNextContext, type InfinitePrevContext, type InfiniteReadApiClient, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UseInfiniteReadResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
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/useInfiniteRead/index.ts
407
+ // src/usePages/index.ts
408
408
  var import_react3 = require("react");
409
409
  var import_core3 = require("@spoosh/core");
410
- function createUseInfiniteRead(options) {
410
+ function createUsePages(options) {
411
411
  const { api, stateManager, eventEmitter, pluginExecutor } = options;
412
- return function useInfiniteRead(readFn, readOptions) {
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
- 'useInfiniteRead requires calling an HTTP method (GET). Example: useInfiniteRead((api) => api("posts").GET())'
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: baseOptionsForKey
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
- if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
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
- baseOptionsForKey,
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: (responses) => mergerRef.current(responses),
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
- query: opts.query,
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 state = (0, import_react3.useSyncExternalStore)(
505
- controller.subscribe,
506
- controller.getState,
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
- controller.mount();
531
- lifecycleRef.current.initialized = true;
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.refetch().finally(() => setIsPending(false));
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.refetch().finally(() => setIsPending(false));
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
- (0, import_react3.useEffect)(() => {
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
- }, [JSON.stringify(pluginOpts)]);
569
- const entry = stateManager.getCache(queryKey);
570
- const pluginResultData = entry?.meta ? Object.fromEntries(entry.meta) : {};
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
- allResponses: state.allResponses,
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: controller.refetch,
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 useInfiniteRead = createUseInfiniteRead({
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
- useInfiniteRead,
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/useInfiniteRead/index.ts
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 createUseInfiniteRead(options) {
420
+ function createUsePages(options) {
420
421
  const { api, stateManager, eventEmitter, pluginExecutor } = options;
421
- return function useInfiniteRead(readFn, readOptions) {
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
- 'useInfiniteRead requires calling an HTTP method (GET). Example: useInfiniteRead((api) => api("posts").GET())'
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: baseOptionsForKey
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
- if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
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
- baseOptionsForKey,
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: (responses) => mergerRef.current(responses),
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
- query: opts.query,
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 state = useSyncExternalStore3(
514
- controller.subscribe,
515
- controller.getState,
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
- controller.mount();
540
- lifecycleRef.current.initialized = true;
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.refetch().finally(() => setIsPending(false));
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.refetch().finally(() => setIsPending(false));
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
- useEffect2(() => {
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
- }, [JSON.stringify(pluginOpts)]);
578
- const entry = stateManager.getCache(queryKey);
579
- const pluginResultData = entry?.meta ? Object.fromEntries(entry.meta) : {};
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
- allResponses: state.allResponses,
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: controller.refetch,
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 useInfiniteRead = createUseInfiniteRead({
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
- useInfiniteRead,
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.11.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.14.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.14.0",
44
- "@spoosh/test-utils": "0.2.0"
43
+ "@spoosh/core": "0.15.0",
44
+ "@spoosh/test-utils": "0.3.0"
45
45
  },
46
46
  "scripts": {
47
47
  "dev": "tsup --watch",