AgentStack
SKILL verified MIT Self-run

React

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

Use when implementing advanced React patterns — designing custom hooks, using Suspense or error boundaries, applying TypeScript generics to components and hooks, or building animated UI with Framer Motion, View Transitions, or scroll-driven animations. For Next.js App Router, Server Components, or Server Actions, use nextjs.

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

Install

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

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

Are you the author of React? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 forwardRef patterns
  • 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.

Versions

  • v0.1.0 Imported from the upstream source.