@spoosh/react 0.11.0 → 0.13.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 +143 -41
- package/dist/index.d.mts +306 -109
- package/dist/index.d.ts +306 -109
- package/dist/index.js +379 -56
- package/dist/index.mjs +390 -57
- package/package.json +11 -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`, `usePages`, and `useSSE`.
|
|
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
|
);
|
|
@@ -158,6 +158,61 @@ function PostList() {
|
|
|
158
158
|
}
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
+
### useSSE
|
|
162
|
+
|
|
163
|
+
Subscribe to real-time data streams using Server-Sent Events (SSE).
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { sse } from "@spoosh/transport-sse";
|
|
167
|
+
|
|
168
|
+
// Setup with SSE transport
|
|
169
|
+
const spoosh = new Spoosh<ApiSchema, Error>("/api").withTransports([sse()]);
|
|
170
|
+
export const { useSSE } = create(spoosh);
|
|
171
|
+
|
|
172
|
+
// Basic subscription
|
|
173
|
+
function Notifications() {
|
|
174
|
+
const { data, isConnected, loading } = useSSE(
|
|
175
|
+
(api) => api("notifications").GET({ query: { userId: "user-123" } })
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (loading) return <div>Connecting...</div>;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div>
|
|
182
|
+
<span>{isConnected ? "Connected" : "Disconnected"}</span>
|
|
183
|
+
{data?.message && <p>{data.message.text}</p>}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Subscribe to specific events only
|
|
189
|
+
const { data } = useSSE(
|
|
190
|
+
(api) => api("notifications").GET({
|
|
191
|
+
query: { userId: "user-123" },
|
|
192
|
+
}),
|
|
193
|
+
{ events: ["alert"] } // Only alert events
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// AI streaming with accumulation
|
|
197
|
+
const { data, trigger } = useSSE(
|
|
198
|
+
(api) => api("chat").POST(),
|
|
199
|
+
{
|
|
200
|
+
events: ["chunk", "done"],
|
|
201
|
+
parse: "json-done",
|
|
202
|
+
accumulate: {
|
|
203
|
+
chunk: (prev, curr) => ({
|
|
204
|
+
...curr,
|
|
205
|
+
chunk: (prev?.chunk || "") + curr.chunk,
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
enabled: false,
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Start streaming on demand
|
|
213
|
+
await trigger({ body: { message: "Hello" } });
|
|
214
|
+
```
|
|
215
|
+
|
|
161
216
|
## API Reference
|
|
162
217
|
|
|
163
218
|
### useRead(readFn, options?)
|
|
@@ -192,41 +247,88 @@ function PostList() {
|
|
|
192
247
|
| `loading` | `boolean` | True while mutation is in progress |
|
|
193
248
|
| `abort` | `() => void` | Abort current request |
|
|
194
249
|
|
|
195
|
-
###
|
|
250
|
+
### usePages(readFn, options)
|
|
196
251
|
|
|
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
|
|
252
|
+
| Option | Type | Required | Description |
|
|
253
|
+
| ----------------- | ---------------------------- | -------- | ------------------------------------------------- |
|
|
254
|
+
| `merger` | `(pages) => TItem[]` | Yes | Merge all pages into items |
|
|
255
|
+
| `canFetchNext` | `(ctx) => boolean` | No | Check if next page exists. Default: `() => false` |
|
|
256
|
+
| `nextPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for next page |
|
|
257
|
+
| `canFetchPrev` | `(ctx) => boolean` | No | Check if previous page exists |
|
|
258
|
+
| `prevPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for previous page |
|
|
259
|
+
| `enabled` | `boolean` | No | Whether to fetch automatically |
|
|
205
260
|
|
|
206
261
|
**Context object passed to callbacks:**
|
|
207
262
|
|
|
208
263
|
```typescript
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
264
|
+
// For canFetchNext and nextPageRequest
|
|
265
|
+
type NextContext<TData, TRequest> = {
|
|
266
|
+
lastPage: InfinitePage<TData> | undefined;
|
|
267
|
+
pages: InfinitePage<TData>[];
|
|
268
|
+
request: TRequest;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// For canFetchPrev and prevPageRequest
|
|
272
|
+
type PrevContext<TData, TRequest> = {
|
|
273
|
+
firstPage: InfinitePage<TData> | undefined;
|
|
274
|
+
pages: InfinitePage<TData>[];
|
|
275
|
+
request: TRequest;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Each page in the pages array
|
|
279
|
+
type InfinitePage<TData> = {
|
|
280
|
+
status: "pending" | "loading" | "success" | "error" | "stale";
|
|
281
|
+
data?: TData;
|
|
282
|
+
error?: TError;
|
|
283
|
+
meta?: TMeta;
|
|
284
|
+
input?: { query?; params?; body? };
|
|
213
285
|
};
|
|
214
286
|
```
|
|
215
287
|
|
|
216
288
|
**Returns:**
|
|
217
289
|
|
|
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`
|
|
290
|
+
| Property | Type | Description |
|
|
291
|
+
| -------------- | ----------------------------- | ----------------------------------------------- |
|
|
292
|
+
| `data` | `TItem[] \| undefined` | Merged items from all pages |
|
|
293
|
+
| `pages` | `InfinitePage<TData>[]` | Array of all pages with status, data, and meta |
|
|
294
|
+
| `loading` | `boolean` | True during initial load |
|
|
295
|
+
| `fetching` | `boolean` | True during any fetch |
|
|
296
|
+
| `fetchingNext` | `boolean` | True while fetching next page |
|
|
297
|
+
| `fetchingPrev` | `boolean` | True while fetching previous |
|
|
298
|
+
| `canFetchNext` | `boolean` | Whether next page exists |
|
|
299
|
+
| `canFetchPrev` | `boolean` | Whether previous page exists |
|
|
300
|
+
| `fetchNext` | `() => Promise<void>` | Fetch the next page |
|
|
301
|
+
| `fetchPrev` | `() => Promise<void>` | Fetch the previous page |
|
|
302
|
+
| `trigger` | `(options?) => Promise<void>` | Trigger fetch with optional new request options |
|
|
303
|
+
| `abort` | `() => void` | Abort current request |
|
|
304
|
+
| `error` | `TError \| undefined` | Error if request failed |
|
|
305
|
+
|
|
306
|
+
### useSSE(subFn, options?)
|
|
307
|
+
|
|
308
|
+
| Option | Type | Default | Description |
|
|
309
|
+
| ------------ | ------------------ | ----------- | --------------------------------- |
|
|
310
|
+
| `enabled` | `boolean` | `true` | Whether to connect automatically |
|
|
311
|
+
| `events` | `string[]` | all events | Subscribe to specific events only |
|
|
312
|
+
| `parse` | `ParseConfig` | `"auto"` | How to parse raw event data |
|
|
313
|
+
| `accumulate` | `AccumulateConfig` | `"replace"` | How to combine events over time |
|
|
314
|
+
|
|
315
|
+
**Returns:**
|
|
316
|
+
|
|
317
|
+
| Property | Type | Description |
|
|
318
|
+
| ------------- | ----------------------- | ------------------------------ |
|
|
319
|
+
| `data` | `TEvents \| undefined` | Accumulated event data |
|
|
320
|
+
| `error` | `TError \| undefined` | Error if connection failed |
|
|
321
|
+
| `loading` | `boolean` | True during initial connection |
|
|
322
|
+
| `isConnected` | `boolean` | True when connected to stream |
|
|
323
|
+
| `trigger` | `(options?) => Promise` | Reconnect with new options |
|
|
324
|
+
| `disconnect` | `() => void` | Disconnect from stream |
|
|
325
|
+
| `reset` | `() => void` | Reset accumulated data |
|
|
326
|
+
|
|
327
|
+
**Connection Options:**
|
|
328
|
+
|
|
329
|
+
| Option | Type | Description |
|
|
330
|
+
| ------------- | -------------------- | ------------------------------------------- |
|
|
331
|
+
| `headers` | `HeadersInit` | Request headers |
|
|
332
|
+
| `credentials` | `RequestCredentials` | Credentials mode |
|
|
333
|
+
| `maxRetries` | `number` | Max retry attempts (default: 3) |
|
|
334
|
+
| `retryDelay` | `number` | Delay between retries in ms (default: 1000) |
|