skills/spoosh-react/SKILL.md
Use this skill when the user asks about "Spoosh", "useRead", "useWrite", "usePages", "useQueue", "useSSE", "createClient", "Spoosh React", "Spoosh hooks", "Spoosh plugins", "cache plugin", "retry plugin", "polling plugin", "optimistic updates", "standalone optimistic", "WebSocket cache update", "invalidation", "devtool", "Next.js SSR", "initialData", "HonoToSpoosh", "ElysiaToSpoosh", "OpenAPI", "data fetching component", "mutation component", "infinite scroll", "Spoosh patterns", or needs to build React components with type-safe API calls. Provides comprehensive API knowledge and component patterns for @spoosh/react.
npx skillsauth add spooshdev/skills spoosh-reactInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Spoosh is a type-safe API toolkit with a composable plugin architecture for TypeScript. This skill covers the React integration including hooks API, plugins, and component patterns.
pnpm add @spoosh/core @spoosh/react
import { Spoosh } from "@spoosh/core";
import { create } from "@spoosh/react";
type ApiSchema = {
users: {
GET: { data: User[] };
POST: { data: User; body: CreateUserBody };
};
"users/:id": {
GET: { data: User };
DELETE: { data: void };
};
};
const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin(),
retryPlugin(),
]);
export const { useRead, useWrite, usePages, useQueue, useSSE } = create(spoosh);
For simple use cases without hooks or plugins:
import { createClient } from "@spoosh/core";
const api = createClient<ApiSchema, Error>("/api");
const { data, error } = await api("users").GET();
const { data: user } = await api("users/:id").GET({ params: { id: "123" } });
await api("users").POST({ body: { name: "John" } });
Fetch data with automatic caching and state management.
const { data, loading, error, trigger } = useRead((api) => api("users").GET(), {
staleTime: 30000,
enabled: true,
});
Returns: data, loading, fetching, error, trigger(), abort(), meta
Options: enabled, tags, staleTime, retry, pollingInterval, refetch, debounce, transform, initialData
Perform mutations (POST, PUT, DELETE).
const { trigger, loading, error } = useWrite((api) => api("users").POST());
// Invalidation is AUTOMATIC - no need to specify in most cases!
await trigger({ body: { name, email } });
Returns: trigger(), loading, error, data, meta, abort()
Trigger options: body, params, query, headers, invalidate, clearCache, optimistic
Bidirectional pagination with infinite scroll.
const { data, fetchNext, canFetchNext, loading } = usePages(
(api) => api("posts").GET({ query: { page: 1 } }),
{
canFetchNext: ({ lastPage }) => lastPage?.data?.hasMore ?? false,
nextPageRequest: ({ lastPage, request }) => ({
query: { ...request.query, page: (lastPage?.data?.page ?? 0) + 1 },
}),
merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
}
);
Returns: data, pages, loading, fetchingNext, canFetchNext, fetchNext(), fetchPrev(), trigger()
Queue management for batch operations with concurrency control.
const { tasks, stats, trigger, retry } = useQueue(
(api) => api("files").POST(),
{ concurrency: 3 }
);
files.forEach((file) => trigger({ body: form({ file }) }));
Returns: tasks, stats, trigger(), abort(), retry(), remove(), removeSettled(), clear(), setConcurrency()
Stats: pending, loading, settled, success, failed, total, percentage
Server-Sent Events for real-time streaming.
const { data, isConnected, trigger, disconnect } = useSSE(
(api) => api("stream").GET(),
{ parse: "json", accumulate: "replace" }
);
Returns: data, error, isConnected, loading, trigger(), disconnect(), reset()
Options: enabled, parse (auto|json|text|json-done), accumulate (replace|merge), maxRetries, retryDelay
| Plugin | Purpose | Key Options |
| -------------------- | ---------------------- | ----------------------------------- |
| cachePlugin | Response caching | staleTime |
| retryPlugin | Automatic retries | retry: { retries, delay } |
| pollingPlugin | Auto-refresh | pollingInterval |
| invalidationPlugin | Cache invalidation | invalidate |
| optimisticPlugin | Instant UI updates + standalone optimistic fn | optimistic |
| debouncePlugin | Debounce requests | debounce |
| refetchPlugin | Refetch on focus | refetch: { onFocus, onReconnect } |
| initialDataPlugin | Preloaded data | initialData |
| devtool | Visual debugging panel | enabled, showFloatingIcon |
import { devtool } from "@spoosh/devtool";
const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin(),
devtool({ enabled: process.env.NODE_ENV === "development" }),
]);
Features: request tracing, plugin visualization, cache inspector, timeline view.
IMPORTANT: Tags and invalidation are handled automatically! You rarely need to configure them manually.
api("users/:id").GET({ params: { id: 123 } }) → tag: "users/123")[firstSegment, firstSegment/*] pattern// ✅ RECOMMENDED: Let Spoosh handle it automatically
await trigger({ body: data });
// POST users/123 → auto-invalidates ["users", "users/*"]
// Only override when you need specific behavior:
await trigger({ body: data, invalidate: "posts" }); // Exact match only
await trigger({ body: data, invalidate: "posts/*" }); // Children only
await trigger({ body: data, invalidate: ["posts", "users"] }); // Multiple patterns
await trigger({ body: data, invalidate: false }); // Disable invalidation
await trigger({ body: data, invalidate: "*" }); // Global refetch all
Custom tags on queries (when auto-generated tag doesn't fit your needs):
// Custom tag only - replaces auto-generated tag
const { data } = useRead((api) => api("users").GET(), {
tags: "dashboard-users",
});
// Default path + custom tags (recommended for cross-cutting concerns)
const { data } = useRead((api) => api("stats").GET(), {
tags: ["stats", "sidebar-stats"], // Keep "stats" for auto-invalidation + add custom
});
// Then invalidate by either:
await trigger({ body: data, invalidate: "stats" }); // Path-based (auto works)
await trigger({ body: data, invalidate: "sidebar-stats" }); // Custom tag
Groups config for namespaced APIs (e.g., admin/posts, api/v1/users):
invalidationPlugin({
groups: ["admin", "api/v1"], // These prefixes use deeper segment matching
});
// POST admin/posts → invalidates ["admin/posts", "admin/posts/*"] instead of ["admin", "admin/*"]
export function UserList() {
const { data, loading, error, trigger } = useRead(
(api) => api("users").GET(),
{ staleTime: 30000 }
);
if (loading) return <UserListSkeleton />;
if (error) return <ErrorMessage error={error} onRetry={trigger} />;
if (!data?.length) return <EmptyState message="No users found" />;
return (
<ul>
{data.map((user) => <UserCard key={user.id} user={user} />)}
</ul>
);
}
export function CreateUserForm() {
const [name, setName] = useState("");
const { trigger, loading, error } = useWrite((api) => api("users").POST());
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// No need to specify invalidate - it's automatic!
const result = await trigger({ body: { name } });
if (result.data) setName("");
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} disabled={loading} />
{error && <p className="error">{error.message}</p>}
<button disabled={loading}>{loading ? "Creating..." : "Create"}</button>
</form>
);
}
export function InfinitePostList() {
const { ref, inView } = useInView();
const { data, loading, fetchingNext, canFetchNext, fetchNext } = usePages(
(api) => api("posts").GET({ query: { page: 1, limit: 20 } }),
{
canFetchNext: ({ lastPage }) => lastPage?.data?.hasMore ?? false,
nextPageRequest: ({ lastPage, request }) => ({
query: { ...request.query, page: (lastPage?.data?.page ?? 0) + 1 }
}),
merger: (pages) => pages.flatMap((p) => p.data?.items ?? [])
}
);
useEffect(() => {
if (inView && canFetchNext && !fetchingNext) fetchNext();
}, [inView, canFetchNext, fetchingNext]);
if (loading) return <PostListSkeleton />;
return (
<div>
{data?.map((post) => <PostCard key={post.id} post={post} />)}
<div ref={ref}>{fetchingNext && <LoadingSpinner />}</div>
</div>
);
}
export function ToggleLikeButton({ postId, liked, likeCount }: Props) {
const { trigger } = useWrite((api) => api("posts/:id/like").POST());
const handleToggle = () => {
trigger({
params: { id: postId },
optimistic: (cache) => cache(`posts/${postId}`)
.set((current) => ({
...current,
liked: !liked,
likeCount: liked ? likeCount - 1 : likeCount + 1
}))
});
};
return <button onClick={handleToggle}>{liked ? "Unlike" : "Like"} ({likeCount})</button>;
}
export function SearchUsers() {
const [query, setQuery] = useState("");
const { data, fetching } = useRead(
(api) => api("users/search").GET({ query: { q: query } }),
{ enabled: query.length >= 2, debounce: 300 }
);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
{fetching && <LoadingIndicator />}
{data?.map((user) => <li key={user.id}>{user.name}</li>)}
</div>
);
}
export function JobStatus({ jobId }: { jobId: string }) {
const { data } = useRead(
(api) => api("jobs/:id").GET({ params: { id: jobId } }),
{
pollingInterval: ({ data }) => {
if (data?.status === "completed" || data?.status === "failed") return false;
return 2000;
}
}
);
return <p>Status: {data?.status}</p>;
}
// app/posts/page.tsx
import { createClient } from "@spoosh/core";
const api = createClient<ApiSchema, Error>(process.env.API_URL!);
export default async function PostsPage() {
const { data: posts } = await api("posts").GET();
return <PostList initialData={posts} />;
}
// components/PostList.tsx
"use client";
export function PostList({ initialData }: { initialData: Post[] }) {
const { data, loading } = useRead(
(api) => api("posts").GET(),
{ initialData } // No loading state on first render
);
return data?.map((post) => <PostCard key={post.id} post={post} />);
}
// lib/spoosh.ts
import { nextjsPlugin } from "@spoosh/plugin-nextjs";
const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin(),
invalidationPlugin(),
nextjsPlugin(),
]);
// After mutation, Next.js cache tags are automatically revalidated
// No need to specify invalidate - it's automatic!
await trigger({ body: newPost });
import { Spoosh, StripPrefix } from "@spoosh/core";
import type { HonoToSpoosh } from "@spoosh/hono";
// Server: app.basePath("/api")
type FullSchema = HonoToSpoosh<typeof app>;
type ApiSchema = StripPrefix<FullSchema, "api">; // Avoid double /api/api
const spoosh = new Spoosh<ApiSchema, Error>("/api");
import { Spoosh, StripPrefix } from "@spoosh/core";
import type { ElysiaToSpoosh } from "@spoosh/elysia";
// Server: new Elysia({ prefix: "/api" })
type FullSchema = ElysiaToSpoosh<typeof app>;
type ApiSchema = StripPrefix<FullSchema, "api">; // Avoid double /api/api
const spoosh = new Spoosh<ApiSchema, Error>("/api");
Use StripPrefix when your baseUrl includes the same prefix as the server's basePath to prevent double prefixing (e.g., /api/api/users).
# Export TypeScript → OpenAPI
npx spoosh-openapi export --schema ./schema.ts --output openapi.json
# Import OpenAPI → TypeScript
npx spoosh-openapi import openapi.json --output ./schema.ts
For detailed API documentation:
references/hooks-api.md - Complete hook signaturesreferences/plugins-api.md - All plugin configurationsreferences/advanced-patterns.md - Complex patterns and edge casesIf more detail needed, fetch https://spoosh.dev/docs/react/llms (or /llms-full for complete docs).
tools
Use this skill when the user asks about "Spoosh", "injectRead", "injectWrite", "injectPages", "injectQueue", "Spoosh Angular", "Spoosh injects", "Spoosh plugins", "cache plugin", "retry plugin", "polling plugin", "optimistic updates", "standalone optimistic", "WebSocket cache update", "invalidation", "data fetching component", "mutation component", "infinite scroll", "Spoosh patterns", or needs to build Angular components with type-safe API calls. Provides comprehensive API knowledge and component patterns for @spoosh/angular.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------