Install
$ agentstack add skill-kid-sid-claude-spellbook-react ✓ 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 Used
- ✓ 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.
About
React — Advanced Patterns
> For state management (Zustand/Redux), TanStack Query, React Hook Form, React Router, and component testing, see the frontend skill.
When to Activate
- Designing or debugging custom hooks
- Choosing between Context, Zustand, or prop drilling
- Using Suspense, error boundaries, or concurrent features (
useTransition,useDeferredValue) - Building compound components or headless/renderless components
- Working in Next.js App Router (Server Components, Server Actions, streaming)
- TypeScript generics, event types, or
forwardRefpatterns useRef,useImperativeHandle, portals, or advanced DOM integration
Hooks Deep Dive
useReducer — when useState gets complex
type State = { count: number; error: string | null; loading: boolean };
type Action =
| { type: "increment" }
| { type: "set_error"; payload: string }
| { type: "reset" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { ...state, count: state.count + 1 };
case "set_error": return { ...state, error: action.payload, loading: false };
case "reset": return { count: 0, error: null, loading: false };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, error: null, loading: false });
dispatch({ type: "increment" });
Use useReducer over useState when: multiple related state fields, next state depends on previous, or actions have semantic names that make logic readable.
useRef — three distinct uses
// 1. DOM access
const inputRef = useRef(null);
useEffect(() => { inputRef.current?.focus(); }, []);
// 2. Mutable value that doesn't trigger re-render (e.g. interval ID, previous value)
const timerRef = useRef(null);
const prevValueRef = useRef(value);
useEffect(() => { prevValueRef.current = value; });
// 3. Stable callback reference (avoids stale closure in event listeners)
const callbackRef = useRef(onSave);
useEffect(() => { callbackRef.current = onSave; });
useEffect(() => {
const handler = () => callbackRef.current();
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []); // dep array stays empty — no stale closure
useContext — subscribe only to what you need
// Split large contexts to prevent unnecessary re-renders
const UserDataContext = createContext(null);
const UserActionsContext = createContext(null);
// Custom hook enforces non-null and co-locates error message
function useUserData() {
const ctx = useContext(UserDataContext);
if (!ctx) throw new Error("useUserData must be used inside UserProvider");
return ctx;
}
// Provider combines both
function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState(null);
const actions = useMemo(() => ({ login: ..., logout: ... }), []); // stable ref
return (
{children}
);
}
Context is NOT a performance-free global store. Every consumer re-renders when the value changes. Use Zustand for frequently-changing shared state; Context for stable config (theme, locale, auth user).
Concurrent Features
// useTransition — mark state update as non-urgent (keeps UI responsive)
const [isPending, startTransition] = useTransition();
function handleSearch(query: string) {
setInputValue(query); // urgent — update input immediately
startTransition(() => {
setFilteredResults(filter(query)); // non-urgent — can be interrupted
});
}
// useDeferredValue — defer a derived value (use when you don't own the state setter)
const deferredQuery = useDeferredValue(searchQuery);
const results = useMemo(() => filter(deferredQuery), [deferredQuery]);
const isStale = searchQuery !== deferredQuery; // show loading indicator
// useId — stable unique IDs across server/client (avoids hydration mismatch)
function FormField({ label }: { label: string }) {
const id = useId();
return (
<>
{label}
);
}
Custom Hooks
Extract logic into a hook when: the same useEffect + useState combo appears twice, or a component mixes UI with data-fetching concerns.
// useLocalStorage — syncs state with localStorage
function useLocalStorage(key: string, initialValue: T) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
const set = useCallback((newValue: T | ((prev: T) => T)) => {
setValue(prev => {
const next = newValue instanceof Function ? newValue(prev) : newValue;
localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);
return [value, set] as const;
}
// useDebounce — delay reacting to fast-changing input
function useDebounce(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useEventListener — typed, auto-cleaned
function useEventListener(
event: K,
handler: (e: WindowEventMap[K]) => void,
element: EventTarget = window,
) {
const handlerRef = useRef(handler);
useEffect(() => { handlerRef.current = handler; });
useEffect(() => {
const fn = (e: Event) => handlerRef.current(e as WindowEventMap[K]);
element.addEventListener(event, fn);
return () => element.removeEventListener(event, fn);
}, [event, element]);
}
Compound Components
Let parent manage state; children access it via Context. No prop drilling, flexible composition.
const TabsContext = createContext void } | null>(null);
const useTabs = () => {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error("Must be used inside ");
return ctx;
};
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
const [active, setActive] = useState(defaultTab);
return (
{children}
);
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const { active, setActive } = useTabs();
return (
setActive(id)}
>
{children}
);
}
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
const { active } = useTabs();
return active === id ? {children} : null;
}
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage
Overview
Settings
Error Boundaries
React errors during render are caught by the nearest error boundary. Must be a class component (or use react-error-boundary library).
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
Something went wrong:
{error.message}
Try again
);
}
// Wrap any subtree that might throw during render
queryClient.resetQueries()}
onError={(error, info) => logger.error(error, info)}
>
Error boundaries do not catch: async errors (use try/catch), event handler errors, or server-side errors.
Suspense
// Suspense shows fallback while children are loading (lazy imports or data)
}>
// Nested Suspense — granular loading states
}>
}>
{/* streams in independently */}
// Combine with ErrorBoundary
}>
forwardRef and useImperativeHandle
// forwardRef — pass ref through to a DOM element
const Input = forwardRef(function Input(props, ref) {
return ;
});
// useImperativeHandle — expose a custom API instead of the raw DOM node
interface DialogHandle { open: () => void; close: () => void }
const Dialog = forwardRef(function Dialog(props, ref) {
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
return open ? {props.children} : null;
});
// Usage
const dialogRef = useRef(null);
dialogRef.current?.open();
Portals
Render outside the component tree (modals, tooltips, toasts) without CSS stacking-context issues.
import { createPortal } from "react-dom";
function Modal({ children, onClose }: ModalProps) {
return createPortal(
e.stopPropagation()}>
{children}
,
document.body, // renders at the bottom of , outside the app root
);
}
TypeScript Patterns
// Generic component
function List({
items,
renderItem,
}: {
items: T[];
renderItem: (item: T) => ReactNode;
}) {
return {items.map(item => {renderItem(item)})};
}
// Event types
const handleChange = (e: React.ChangeEvent) => setValue(e.target.value);
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); ... };
const handleClick = (e: React.MouseEvent) => { ... };
// Polymorphic component (renders as any element)
type ButtonProps = {
as?: T;
children: ReactNode;
} & ComponentPropsWithoutRef;
function Button({ as, children, ...props }: ButtonProps) {
const Component = as ?? "button";
return {children};
}
// Go home
// Children utilities
type WithChildren = T & { children: ReactNode };
type WithOptionalChildren = T & { children?: ReactNode };
Next.js App Router
Server vs Client Components
// Server Component (default) — runs on server, no hooks, no browser APIs
// app/users/page.tsx
export default async function UsersPage() {
const users = await db.user.findMany(); // direct DB access — no API round-trip
return ;
}
// Client Component — add "use client" directive, can use hooks
"use client";
import { useState } from "react";
export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [value, setValue] = useState("");
return { setValue(e.target.value); onSearch(e.target.value); }} />;
}
// Pattern: Server Component wraps Client Component
// Server fetches data, Client handles interactivity
export default async function Page() {
const initialData = await fetchData();
return ; // Client Component
}
Server Actions
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
await db.user.create({ data: { name } });
revalidatePath("/users"); // bust the cache for this route
}
// Use directly in a Server Component form
Create
// Or call from a Client Component
"use client";
import { createUser } from "./actions";
import { useFormState, useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return {pending ? "Saving…" : "Save"};
}
export function UserForm() {
const [state, action] = useFormState(createUser, null);
return ;
}
Streaming with Suspense
// app/dashboard/page.tsx — stream slow sections independently
import { Suspense } from "react";
export default function DashboardPage() {
return (
{/* renders immediately */}
}>
{/* streams in when ready */}
}>
{/* streams independently */}
);
}
Metadata and Caching
// Static metadata
export const metadata: Metadata = { title: "Users", description: "Manage users" };
// Dynamic metadata
export async function generateMetadata({ params }: { params: { id: string } }): Promise {
const user = await fetchUser(params.id);
return { title: user.name };
}
// Fetch caching options
fetch(url, { cache: "no-store" }); // always fresh (SSR)
fetch(url, { next: { revalidate: 60 } }); // ISR — revalidate every 60 seconds
fetch(url); // default: cached (SSG)
Common Pitfalls
| Pitfall | Fix | |---|---| | useEffect with missing deps | Add all deps; extract stable refs with useRef if needed | | Stale closure in event listener | Store callback in useRef, reference in handler | | Context re-rendering all consumers | Split into data + actions contexts; memoize value | | key on wrong element | Put key on the outermost element returned by map, not inside it | | Mutating state directly | Always return new object/array from useState setter | | async in useEffect directly | Declare async inner function, call it immediately | | Server Component importing Client Component that imports server-only code | Use server-only package or restructure imports |
Animation
Library Choice
| Library | Best for | Bundle | |---|---|---| | Framer Motion | Rich gestures, layout, shared element transitions | ~50kb | | Motion (lightweight) | Simple enter/exit, lower bundle cost | ~18kb | | React Spring | Physics-based, natural feel | ~45kb | | CSS + Tailwind | Simple transitions, no JS needed | 0kb | | View Transitions API | Page/route transitions (native browser) | 0kb |
Rule: reach for CSS first, Framer Motion when you need gestures, layout animations, or AnimatePresence.
Framer Motion — Core Patterns
import { motion, AnimatePresence } from "framer-motion"
// Basic animate — from initial to animate on mount
// Exit animation — must be wrapped in AnimatePresence
{isVisible && (
)}
// Hover / tap — no useState needed
Click me
Variants — orchestrate child animations
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.08 }, // children appear 80ms apart
},
}
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0 },
}
{items.map(i => (
{i.name}
))}
Layout animations — animate position/size changes automatically
// Add layoutId to animate an element moving between positions
// (shared element transition — card expands to modal)
// In the expanded view:
// layout prop — animate any layout change (reorder, resize)
{/* Reorder items — Framer animates the position change */}
Gestures — drag
{
if (info.offset.x > 100) dismiss()
}}
/>
Scroll-triggered animations
import { motion, useInView } from "framer-motion"
import { useRef } from "react"
function FadeInSection({ children }: { children: ReactNode }) {
const ref = useRef(null)
const inView = useInView(ref, { once: true, margin: "-100px" })
return (
{children}
)
}
useMotionValue + useTransform — scroll parallax
import { useScroll, useTransform, motion } from "framer-motion"
function ParallaxHero() {
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 500], [0, -150]) // scroll 500px → move -150px
return (
)
}
CSS View Transitions API — page transitions
Native browser API for animating between page states. No library needed.
// Next.js App Router — use next-view-transitions
// npm install next-view-transitions
import { ViewTransitions } from "next-view-transitions"
// app/layout.tsx
export default function RootLayout({ children }) {
return (
{children}
)
}
// Use from next-view-transitions (not next/link)
import { Link } from "next-view-transitions"
About
/* Default cross-fade — customize with CSS */
::view-transition-old(root) {
animation: 200ms ease fade-out;
}
::view-transition-new(root) {
animation: 200ms ease fade-in;
}
/* Named transition — shared element between pages */
.hero-image { view-transition-name: hero; }
CSS @starting-style — enter animations without JS
Animates an element from a style on its first render. No library, no useEffect.
/* Button fades in when it appears in the DOM */
.toast {
opacity: 1;
transition: opacity 0.3s ease;
}
@starting-style {
.toast {
opacity: 0; /* value when first painted */
}
}
`
…
## 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](https://github.com/kid-sid)
- **Source:** [kid-sid/claude-spellbook](https://github.com/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.