Install
$ agentstack add skill-kid-sid-claude-spellbook-frontend ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Frontend Patterns
Conventions and best practices for building maintainable, performant React applications.
When to Activate
- Designing component structure or deciding where state should live
- Choosing between local state, Context, Zustand, or Redux Toolkit
- Implementing data fetching, caching, or server state synchronization
- Building forms with validation and submission handling
- Optimizing rendering performance (re-renders, bundle size, lazy loading)
- Setting up routing with protected routes or nested layouts
- Writing tests for React components and hooks
Component Design
Single Responsibility
// BAD: component does too many things
function UserDashboard({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [orders, setOrders] = useState([]);
useEffect(() => { /* fetch user + orders */ }, []);
return {/* render user, orders, sidebar, notifications */};
}
// GOOD: split by concern
function UserDashboard({ userId }: { userId: string }) {
return (
);
}
Composition over Props Drilling
// BAD: prop drilling through 3+ levels
// GOOD: compound component or slot pattern
{children}
Container / Presentational Split
// Container: owns data fetching and state
function OrderListContainer({ userId }: { userId: string }) {
const { data, isLoading, error } = useOrders(userId);
if (isLoading) return ;
if (error) return ;
return ;
}
// Presentational: pure rendering, easily testable
function OrderList({ orders }: { orders: Order[] }) {
return (
{orders.map(o => )}
);
}
State Management
Decision Matrix
| State Type | Tool | Examples | |---|---|---| | UI / ephemeral | useState / useReducer | modal open, input value, toggle | | Shared UI state | Zustand or Context | theme, sidebar collapsed, selected tab | | Server state | TanStack Query | API responses, lists, detail pages | | Complex client state | Redux Toolkit | shopping cart, multi-step wizard, offline queue | | URL state | Search params | filters, pagination, selected item | | Form state | React Hook Form | field values, validation, submission |
Zustand (preferred for shared UI state)
import { create } from "zustand";
interface SidebarStore {
isOpen: boolean;
toggle: () => void;
open: () => void;
close: () => void;
}
const useSidebarStore = create((set) => ({
isOpen: false,
toggle: () => set((s) => ({ isOpen: !s.isOpen })),
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
}));
// With persistence
import { persist } from "zustand/middleware";
const useThemeStore = create(persist(
(set) => ({ theme: "light", setTheme: (t) => set({ theme: t }) }),
{ name: "theme-storage" },
));
Redux Toolkit (complex client state)
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
const fetchOrders = createAsyncThunk("orders/fetch", async (userId: string) => {
const res = await fetch(`/api/users/${userId}/orders`);
return res.json();
});
const ordersSlice = createSlice({
name: "orders",
initialState: { items: [] as Order[], status: "idle" as LoadingStatus },
reducers: {
removeOrder: (state, action) => {
state.items = state.items.filter(o => o.id !== action.payload);
},
},
extraReducers: (builder) => {
builder
.addCase(fetchOrders.pending, (state) => { state.status = "loading"; })
.addCase(fetchOrders.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
})
.addCase(fetchOrders.rejected, (state) => { state.status = "failed"; });
},
});
Data Fetching
TanStack Query (server state)
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// Fetching
function useOrders(userId: string) {
return useQuery({
queryKey: ["orders", userId],
queryFn: () => fetchOrders(userId),
staleTime: 5 * 60 * 1000, // 5 min — don't refetch if fresh
gcTime: 10 * 60 * 1000, // 10 min — keep in cache after unmount
});
}
// Mutation with optimistic update
function useCancelOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (orderId: string) => cancelOrder(orderId),
onMutate: async (orderId) => {
await queryClient.cancelQueries({ queryKey: ["orders"] });
const prev = queryClient.getQueryData(["orders"]);
queryClient.setQueryData(["orders"], (old: Order[]) =>
old.filter(o => o.id !== orderId),
);
return { prev };
},
onError: (_, __, ctx) => {
queryClient.setQueryData(["orders"], ctx?.prev);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ["orders"] }),
});
}
Query Key Conventions
// Hierarchical keys enable targeted invalidation
["users"] // all users
["users", userId] // one user
["users", userId, "orders"] // user's orders
["users", userId, "orders", { status: "active" }] // filtered
queryClient.invalidateQueries({ queryKey: ["users", userId] });
// ^ invalidates user + all sub-keys (orders, profile, etc.)
Forms
React Hook Form + Zod
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "At least 8 characters"),
role: z.enum(["admin", "user"]),
});
type FormValues = z.infer;
function CreateUserForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } =
useForm({ resolver: zodResolver(schema) });
const onSubmit = async (data: FormValues) => {
await createUser(data);
};
return (
{errors.email && {errors.email.message}}
{errors.password && {errors.password.message}}
{isSubmitting ? "Creating…" : "Create User"}
);
}
Routing (React Router v6)
Nested Layouts with Protected Routes
import { createBrowserRouter, RouterProvider, Outlet, Navigate } from "react-router-dom";
function RequireAuth() {
const { user } = useAuth();
return user ? : ;
}
const router = createBrowserRouter([
{ path: "/login", element: },
{
element: ,
children: [
{
element: , // persistent layout (nav, sidebar)
children: [
{ path: "/", element: },
{ path: "/orders", element: },
{ path: "/orders/:id", element: ,
loader: ({ params }) => fetchOrder(params.id!) },
],
},
],
},
]);
URL State for Filters
import { useSearchParams } from "react-router-dom";
function OrderFilters() {
const [params, setParams] = useSearchParams();
const status = params.get("status") ?? "all";
return (
setParams(p => { p.set("status", e.target.value); return p; })
}>
All
Active
);
}
Performance
Memoization — when it helps
// useMemo: expensive derivation, stable reference for children
const sortedOrders = useMemo(
() => [...orders].sort((a, b) => b.total - a.total),
[orders],
);
// useCallback: stable reference passed to memo'd child or as dep
const handleCancel = useCallback((id: string) => cancelOrder(id), [cancelOrder]);
// memo: skip re-render when props unchanged
const OrderItem = memo(function OrderItem({ order }: { order: Order }) {
return {order.id};
});
// BAD: wrapping everything — memo adds overhead, only helps when re-renders are expensive
Code Splitting
import { lazy, Suspense } from "react";
// Route-level splitting (most impactful)
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Reports = lazy(() => import("./pages/Reports"));
function App() {
return (
}>
} />
} />
);
}
Virtualization for Long Lists
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualOrderList({ orders }: { orders: Order[] }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: orders.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 64,
});
return (
{virtualizer.getVirtualItems().map(item => (
))}
);
}
Testing
Component Tests (React Testing Library)
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("submits form with valid data", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render();
await user.type(screen.getByLabelText("Email"), "alice@example.com");
await user.type(screen.getByLabelText("Password"), "secretpass");
await user.click(screen.getByRole("button", { name: "Create User" }));
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "secretpass",
}));
});
test("shows validation errors", async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole("button", { name: "Create User" }));
expect(await screen.findByText("Invalid email")).toBeInTheDocument();
});
Testing Hooks
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
> See also: unit-testing, accessibility
Red Flags
- Data fetching inside
useEffectwithout a library — manual fetch-in-effect produces race conditions, missing loading/error states, and no deduplication; use TanStack Query or SWR - Global state for server data — storing server-fetched data in Redux/Zustand duplicates cache logic already solved by a data fetching library; keep server state in the fetching layer
key={index}in lists — using array index as key breaks React reconciliation when items reorder or are inserted; use a stable, unique ID from the data- Uncontrolled forms for complex validation — uncontrolled inputs with
refcan't drive real-time validation or conditional fields; use React Hook Form with Zod schema validation useEffectto sync derived state — computing derived values in an effect causes an extra render cycle; compute them inline during render or memoize withuseMemo- Prop drilling more than 2 levels — passing props through 3+ components is a sign the tree needs restructuring or a context/selector; don't reach for global state before considering composition
- No
Suspenseboundary around lazy-loaded routes — code-split routes without a fallback show a blank screen during load; wrap every lazy route in}>
Checklist
- [ ] Components have a single clear responsibility — split if rendering + fetching + formatting
- [ ] Server state managed with TanStack Query, not
useEffect+useState - [ ] Forms use React Hook Form with Zod schema validation
- [ ] Shared client state uses Zustand (simple) or Redux Toolkit (complex)
- [ ] URL state (filters, pagination, selected ID) stored in search params
- [ ] Route-level code splitting applied to all page components
- [ ] Lists over ~100 items virtualized
- [ ]
useMemo/useCallback/memoapplied only where profiling confirms re-render cost - [ ] Components tested via React Testing Library (user interactions, not implementation)
- [ ] Accessible: semantic HTML, labels on inputs, keyboard navigation verified
- [ ] No prop drilling beyond 2 levels — use composition, Context, or Zustand
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kid-sid
- Source: kid-sid/claude-spellbook
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.