@spoosh/react
v0.14.1
Published
React hooks for Spoosh API toolkit
Downloads
1,083
Maintainers
Readme
@spoosh/react
React hooks for Spoosh - useRead, useWrite, usePages, and useSSE.
Documentation · Requirements: TypeScript >= 5.0, React >= 18.0
Installation
npm install @spoosh/core @spoosh/reactUsage
Setup
import { Spoosh } from "@spoosh/core";
import { create } from "@spoosh/react";
import { cachePlugin } from "@spoosh/plugin-cache";
const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin({ staleTime: 5000 }),
]);
export const { useRead, useWrite, usePages } = create(spoosh);useRead
Fetch data with automatic caching and refetching.
function UserList() {
const { data, loading, error, trigger } = useRead(
(api) => api("users").GET()
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.map((user) => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// With options
const { data } = useRead(
(api) => api("users").GET({ query: { page: 1 } }),
{
staleTime: 10000,
enabled: isReady,
}
);
// With path parameters
const { data: user } = useRead(
(api) => api("users/:id").GET({ params: { id: userId } }),
{ enabled: !!userId }
);useWrite
Trigger mutations with loading and error states.
function CreateUser() {
const { trigger, loading, error } = useWrite(
(api) => api("users").POST()
);
const handleSubmit = async (data: CreateUserBody) => {
const result = await trigger({ body: data });
if (result.data) {
// Success
}
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button disabled={loading}>
{loading ? "Creating..." : "Create User"}
</button>
</form>
);
}
// With path parameters
const updateUser = useWrite((api) => api("users/:id").PUT());
await updateUser.trigger({
params: { id: userId },
body: { name: "Updated Name" },
});usePages
Bidirectional paginated data fetching with infinite scroll support.
function PostList() {
const {
data,
pages,
loading,
canFetchNext,
canFetchPrev,
fetchNext,
fetchPrev,
fetchingNext,
fetchingPrev,
} = usePages(
(api) => api("posts").GET({ query: { page: 1 } }),
{
// Required: Check if next page exists
canFetchNext: ({ lastPage }) => lastPage?.data?.meta.hasMore ?? false,
// Required: Build request for next page
nextPageRequest: ({ lastPage }) => ({
query: { page: (lastPage?.data?.meta.page ?? 0) + 1 },
}),
// Required: Merge all pages into items
merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
// Optional: Check if previous page exists
canFetchPrev: ({ firstPage }) => (firstPage?.data?.meta.page ?? 1) > 1,
// Optional: Build request for previous page
prevPageRequest: ({ firstPage }) => ({
query: { page: (firstPage?.data?.meta.page ?? 2) - 1 },
}),
}
);
return (
<div>
{canFetchPrev && (
<button onClick={fetchPrev} disabled={fetchingPrev}>
{fetchingPrev ? "Loading..." : "Load Previous"}
</button>
)}
{data?.map((post) => <PostCard key={post.id} post={post} />)}
{canFetchNext && (
<button onClick={fetchNext} disabled={fetchingNext}>
{fetchingNext ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}useSSE
Subscribe to real-time data streams using Server-Sent Events (SSE).
import { sse } from "@spoosh/transport-sse";
// Setup with SSE transport
const spoosh = new Spoosh<ApiSchema, Error>("/api").withTransports([sse()]);
export const { useSSE } = create(spoosh);
// Basic subscription
function Notifications() {
const { data, isConnected, loading } = useSSE(
(api) => api("notifications").GET({ query: { userId: "user-123" } })
);
if (loading) return <div>Connecting...</div>;
return (
<div>
<span>{isConnected ? "Connected" : "Disconnected"}</span>
{data?.message && <p>{data.message.text}</p>}
</div>
);
}
// Subscribe to specific events only
const { data } = useSSE(
(api) => api("notifications").GET({
query: { userId: "user-123" },
}),
{ events: ["alert"] } // Only alert events
);
// AI streaming with accumulation
const { data, trigger } = useSSE(
(api) => api("chat").POST(),
{
events: ["chunk", "done"],
parse: "json-done",
accumulate: {
chunk: (prev, curr) => ({
...curr,
chunk: (prev?.chunk || "") + curr.chunk,
}),
},
enabled: false,
}
);
// Start streaming on demand
await trigger({ body: { message: "Hello" } });API Reference
useRead(readFn, options?)
| Option | Type | Default | Description |
| ---------------- | --------- | ------- | ------------------------------------ |
| enabled | boolean | true | Whether to fetch automatically |
| staleTime | number | - | Cache stale time (from plugin-cache) |
| retries | number | - | Retry attempts (from plugin-retry) |
| + plugin options | - | - | Options from installed plugins |
Returns:
| Property | Type | Description |
| ---------- | --------------------- | ------------------------ |
| data | TData \| undefined | Response data |
| error | TError \| undefined | Error if request failed |
| loading | boolean | True during initial load |
| fetching | boolean | True during any fetch |
| trigger | () => Promise | Manually trigger fetch |
| abort | () => void | Abort current request |
useWrite(writeFn)
Returns:
| Property | Type | Description |
| --------- | ---------------------- | ---------------------------------- |
| trigger | (options) => Promise | Execute the mutation |
| data | TData \| undefined | Response data |
| error | TError \| undefined | Error if request failed |
| loading | boolean | True while mutation is in progress |
| abort | () => void | Abort current request |
usePages(readFn, options)
| Option | Type | Required | Description |
| ----------------- | ---------------------------- | -------- | ------------------------------------------------- |
| merger | (pages) => TItem[] | Yes | Merge all pages into items |
| canFetchNext | (ctx) => boolean | No | Check if next page exists. Default: () => false |
| nextPageRequest | (ctx) => Partial<TRequest> | No | Build request for next page |
| canFetchPrev | (ctx) => boolean | No | Check if previous page exists |
| prevPageRequest | (ctx) => Partial<TRequest> | No | Build request for previous page |
| enabled | boolean | No | Whether to fetch automatically |
Context object passed to callbacks:
// For canFetchNext and nextPageRequest
type NextContext<TData, TRequest> = {
lastPage: InfinitePage<TData> | undefined;
pages: InfinitePage<TData>[];
request: TRequest;
};
// For canFetchPrev and prevPageRequest
type PrevContext<TData, TRequest> = {
firstPage: InfinitePage<TData> | undefined;
pages: InfinitePage<TData>[];
request: TRequest;
};
// Each page in the pages array
type InfinitePage<TData> = {
status: "pending" | "loading" | "success" | "error" | "stale";
data?: TData;
error?: TError;
meta?: TMeta;
input?: { query?; params?; body? };
};Returns:
| Property | Type | Description |
| -------------- | ----------------------------- | ----------------------------------------------- |
| data | TItem[] \| undefined | Merged items from all pages |
| pages | InfinitePage<TData>[] | Array of all pages with status, data, and meta |
| loading | boolean | True during initial load |
| fetching | boolean | True during any fetch |
| fetchingNext | boolean | True while fetching next page |
| fetchingPrev | boolean | True while fetching previous |
| canFetchNext | boolean | Whether next page exists |
| canFetchPrev | boolean | Whether previous page exists |
| fetchNext | () => Promise<void> | Fetch the next page |
| fetchPrev | () => Promise<void> | Fetch the previous page |
| trigger | (options?) => Promise<void> | Trigger fetch with optional new request options |
| abort | () => void | Abort current request |
| error | TError \| undefined | Error if request failed |
useSSE(subFn, options?)
| Option | Type | Default | Description |
| ------------ | ------------------ | ----------- | --------------------------------- |
| enabled | boolean | true | Whether to connect automatically |
| events | string[] | all events | Subscribe to specific events only |
| parse | ParseConfig | "auto" | How to parse raw event data |
| accumulate | AccumulateConfig | "replace" | How to combine events over time |
Returns:
| Property | Type | Description |
| ------------- | ----------------------- | ------------------------------ |
| data | TEvents \| undefined | Accumulated event data |
| error | TError \| undefined | Error if connection failed |
| loading | boolean | True during initial connection |
| isConnected | boolean | True when connected to stream |
| trigger | (options?) => Promise | Reconnect with new options |
| disconnect | () => void | Disconnect from stream |
| reset | () => void | Reset accumulated data |
Connection Options:
| Option | Type | Description |
| ------------- | -------------------- | ------------------------------------------- |
| headers | HeadersInit | Request headers |
| credentials | RequestCredentials | Credentials mode |
| maxRetries | number | Max retry attempts (default: 3) |
| retryDelay | number | Delay between retries in ms (default: 1000) |
