| name | react |
| description | 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. |
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
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const prevValueRef = useRef(value);
useEffect(() => { prevValueRef.current = value; });
const callbackRef = useRef(onSave);
useEffect(() => { callbackRef.current = onSave; });
useEffect(() => {
const handler = () => callbackRef.current();
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
useContext — subscribe only to what you need
const UserDataContext = createContext<UserData | null>(null);
const UserActionsContext = createContext<UserActions | null>(null);
function useUserData() {
const ctx = useContext(UserDataContext);
if (!ctx) throw new Error("useUserData must be used inside UserProvider");
return ctx;
}
function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<UserData | null>(null);
const actions = useMemo(() => ({ login: ..., logout: ... }), []);
return (
<UserDataContext.Provider value={user}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserDataContext.Provider>
);
}
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
const [isPending, startTransition] = useTransition();
function handleSearch(query: string) {
setInputValue(query);
startTransition(() => {
setFilteredResults(filter(query));
});
}
const deferredQuery = useDeferredValue(searchQuery);
const results = useMemo(() => filter(deferredQuery), [deferredQuery]);
const isStale = searchQuery !== deferredQuery;
function FormField({ label }: { label: string }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}
Custom Hooks
Extract logic into a hook when: the same useEffect + useState combo appears twice, or a component mixes UI with data-fetching concerns.
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
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;
}
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function useEventListener<K extends keyof WindowEventMap>(
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<{ active: string; setActive: (id: string) => void } | null>(null);
const useTabs = () => {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error("Must be used inside <Tabs>");
return ctx;
};
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
const [active, setActive] = useState(defaultTab);
return (
<TabsContext.Provider value={{ active, setActive }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const { active, setActive } = useTabs();
return (
<button
role="tab"
aria-selected={active === id}
onClick={() => setActive(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
const { active } = useTabs();
return active === id ? <div role="tabpanel">{children}</div> : null;
}
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
<Tabs defaultTab="overview">
<Tabs.Tab id="overview">Overview</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
<Tabs.Panel id="overview"><OverviewContent /></Tabs.Panel>
<Tabs.Panel id="settings"><SettingsContent /></Tabs.Panel>
</Tabs>
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 (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => queryClient.resetQueries()}
onError={(error, info) => logger.error(error, info)}
>
<UserDashboard />
</ErrorBoundary>
Error boundaries do not catch: async errors (use try/catch), event handler errors, or server-side errors.
Suspense
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
<Suspense fallback={<PageSkeleton />}>
<PageHeader />
<Suspense fallback={<TableSkeleton />}>
<DataTable /> {/* streams in independently */}
</Suspense>
</Suspense>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Spinner />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
forwardRef and useImperativeHandle
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
interface DialogHandle { open: () => void; close: () => void }
const Dialog = forwardRef<DialogHandle, DialogProps>(function Dialog(props, ref) {
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
return open ? <div>{props.children}</div> : null;
});
const dialogRef = useRef<DialogHandle>(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(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body,
);
}
TypeScript Patterns
function List<T extends { id: string }>({
items,
renderItem,
}: {
items: T[];
renderItem: (item: T) => ReactNode;
}) {
return <ul>{items.map(item => <li key={item.id}>{renderItem(item)}</li>)}</ul>;
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); ... };
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... };
type ButtonProps<T extends ElementType = "button"> = {
as?: T;
children: ReactNode;
} & ComponentPropsWithoutRef<T>;
function Button<T extends ElementType = "button">({ as, children, ...props }: ButtonProps<T>) {
const Component = as ?? "button";
return <Component {...props}>{children}</Component>;
}
type WithChildren<T = {}> = T & { children: ReactNode };
type WithOptionalChildren<T = {}> = T & { children?: ReactNode };
Next.js App Router
Server vs Client Components
export default async function UsersPage() {
const users = await db.user.findMany();
return <UserList users={users} />;
}
"use client";
import { useState } from "react";
export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [value, setValue] = useState("");
return <input value={value} onChange={e => { setValue(e.target.value); onSearch(e.target.value); }} />;
}
export default async function Page() {
const initialData = await fetchData();
return <InteractiveWidget initialData={initialData} />;
}
Server Actions
"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");
}
<form action={createUser}>
<input name="name" />
<button type="submit">Create</button>
</form>
"use client";
import { createUser } from "./actions";
import { useFormState, useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}
export function UserForm() {
const [state, action] = useFormState(createUser, null);
return <form action={action}><SubmitButton /></form>;
}
Streaming with Suspense
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div>
<PageHeader /> {/* renders immediately */}
<Suspense fallback={<StatsSkeleton />}>
<SlowStats /> {/* streams in when ready */}
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed /> {/* streams independently */}
</Suspense>
</div>
);
}
Metadata and Caching
export const metadata: Metadata = { title: "Users", description: "Manage users" };
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const user = await fetchUser(params.id);
return { title: user.name };
}
fetch(url, { cache: "no-store" });
fetch(url, { next: { revalidate: 60 } });
fetch(url);
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"
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
/>
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
Click me
</motion.button>
Variants — orchestrate child animations
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.08 },
},
}
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0 },
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map(i => (
<motion.li key={i.id} variants={item}>{i.name}</motion.li>
))}
</motion.ul>
Layout animations — animate position/size changes automatically
<motion.div layoutId={`card-${id}`} className="card" onClick={expand} />
<motion.div layoutId={`card-${id}`} className="modal" />
<motion.div layout>
{/* Reorder items — Framer animates the position change */}
</motion.div>
Gestures — drag
<motion.div
drag
drag="x"
dragConstraints={{ left: -100, right: 100 }}
dragElastic={0.1}
onDragEnd={(_, info) => {
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 (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 24 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease: "easeOut" }}
>
{children}
</motion.div>
)
}
useMotionValue + useTransform — scroll parallax
import { useScroll, useTransform, motion } from "framer-motion"
function ParallaxHero() {
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 500], [0, -150])
return (
<motion.div style={{ y }} className="hero-image" />
)
}
CSS View Transitions API — page transitions
Native browser API for animating between page states. No library needed.
import { ViewTransitions } from "next-view-transitions"
export default function RootLayout({ children }) {
return (
<html>
<body>
<ViewTransitions>{children}</ViewTransitions>
</body>
</html>
)
}
import { Link } from "next-view-transitions"
<Link href="/about">About</Link>
::view-transition-old(root) {
animation: 200ms ease fade-out;
}
::view-transition-new(root) {
animation: 200ms ease fade-in;
}
.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.
.toast {
opacity: 1;
transition: opacity 0.3s ease;
}
@starting-style {
.toast {
opacity: 0;
}
}
Works in Chrome 117+, Firefox 129+. Use Framer Motion as fallback for Safari.
Performance Rules
{ opacity: 0, scale: 0.95, x: -20, y: 20, rotate: 5 }
{ width: 0, height: 0, margin: 0, padding: 0 }
<motion.div style={{ willChange: "transform, opacity" }} />
<motion.div layout />
Spring Config Reference
{ type: "spring", stiffness: 400, damping: 17 }
{ type: "spring", stiffness: 200, damping: 25 }
{ type: "spring", stiffness: 300, damping: 10, mass: 0.5 }
{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }
Red Flags
useEffect with empty [] deps that closes over changing values — an empty dep array on an effect that references props or state silently uses stale data on re-render; add correct deps or use a ref
- Large context that re-renders all consumers on any state change — a monolithic context causes every consumer to re-render on every value change; split by update frequency or use a selector
- Derived state stored in
useState — state computable from props or other state causes stale value bugs; compute it inline during render or memoize with useMemo
React.memo applied everywhere as a premature optimization — wrapping every component in memo adds comparison overhead without benefit when props change every render; profile first, memoize surgically
forwardRef + useImperativeHandle for parent-to-child communication — exposing an imperative handle inverts the data flow; prefer lifting state, callbacks, or composition
- Server Component fetching data that's also fetched by its Client Component child — data fetched in a Server Component passed as props then re-fetched in the Client Component causes duplicate requests; pick one fetch location
<Suspense> without an <ErrorBoundary> — a thrown error in a suspended or lazy component without an error boundary crashes the entire tree; wrap every <Suspense> with an <ErrorBoundary>
Checklist
See also: frontend (state management, TanStack Query, forms, routing, testing), accessibility