- 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
```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