| name | react |
| description | Use when building advanced React features — designing custom hooks, using Suspense or error boundaries, working in Next.js App Router with Server Components or Server Actions, applying TypeScript generics, or building animated UI with Framer Motion, View Transitions, or scroll-driven animations. |
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