React hooks for Spoosh - useRead, useWrite, usePages, and useSSE.
Documentation · Requirements: TypeScript >= 5.0, React >= 18.0
npm install @spoosh/core @spoosh/reactimport { 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);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 }
);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" },
});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>
);
}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" } });| 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 |
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 |
| 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 |
| 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) |