AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Frontend

skill-kid-sid-claude-spellbook-frontend · by kid-sid

Use when making architecture-level React decisions — component decomposition, choosing where state should live, selecting a state manager (Context vs Redux vs Zustand) or a data-fetching library (TanStack Query vs SWR), or planning a rendering-performance budget. For advanced hook patterns, use react.

No reviews yet
0 installs
39 views
0.0% view→install

Install

$ agentstack add skill-kid-sid-claude-spellbook-frontend

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kid-sid-claude-spellbook-frontend)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Frontend? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 useEffect without 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 ref can't drive real-time validation or conditional fields; use React Hook Form with Zod schema validation
  • useEffect to sync derived state — computing derived values in an effect causes an extra render cycle; compute them inline during render or memoize with useMemo
  • 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 Suspense boundary 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 / memo applied 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.