- 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
```tsx
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
```tsx
// 1. DOM access
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
// 2. Mutable value that doesn't trigger re-render (e.g. interval ID, previous value)
const timerRef = useRef<NodeJS.Timeout | null>(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
```tsx
// Split large contexts to prevent unnecessary re-renders
const UserDataContext = createContext<UserData | null>(null);
const UserActionsContext = createContext<UserActions | null>(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<UserData | null>(null);
const actions = useMemo(() => ({ login: ..., logout: ... }), []); // stable ref
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
```tsx
// 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 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.
```tsx
// useLocalStorage — syncs state with localStorage
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;
}
// useDebounce — delay reacting to fast-changing input
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;
}
// useEventListener — typed, auto-cleaned
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.
```tsx
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;
// Usage
<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).
```tsx
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>
);
}
// Wrap any subtree that might throw during render
<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
```tsx
// Suspense shows fallback while children are loading (lazy imports or data)
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
// Nested Suspense — granular loading states
<Suspense fallback={<PageSkeleton />}>
<PageHeader />
<Suspense fallback={<TableSkeleton />}>
<DataTable /> {/* streams in independently */}
</Suspense>
</Suspense>
// Combine with ErrorBoundary
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Spinner />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
```
---
## forwardRef and useImperativeHandle
```tsx
// forwardRef — pass ref through to a DOM element
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
// useImperativeHandle — expose a custom API instead of the raw DOM node
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;
});
// Usage
const dialogRef = useRef<DialogHandle>(null);
dialogRef.current?.open();
```
---
## Portals
Render outside the component tree (modals, tooltips, toasts) without CSS stacking-context issues.
```tsx
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, // renders at the bottom of <body>, outside the app root
);
}
```
---
## TypeScript Patterns
```tsx
// Generic component
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>;
}
// Event types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); ... };
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... };
// Polymorphic component (renders as any element)
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>;
}
View on GitHub