
React Patterns
FreeBuild robust and accessible React components with best practices.
Free · Opens the source repo
What React Patterns does
React Patterns provides a comprehensive guide to idiomatic practices for developing with React 18 and 19. This skill is particularly useful for developers looking to enhance their understanding of modern React patterns, including hooks discipline, server/client component boundaries, and effective state management. By leveraging these patterns, you can create component trees that are not only performant but also accessible, ensuring a better user experience.
The skill emphasizes the importance of pure functions in rendering, side effects management, and the principle of composition over inheritance. It guides users through best practices for writing and modifying React components, custom hooks, and component trees, making it an essential tool for both new and experienced developers. Whether you're migrating from class components or integrating new features in your applications, this skill will provide the insights needed to make informed decisions.
Additionally, React Patterns covers essential topics such as data fetching strategies, implementing forms with React 19 actions, and using Suspense and error boundaries effectively. It includes decision trees for state management and data fetching, helping developers choose the right approach based on their application's needs. This skill is designed for those who want to write cleaner, more maintainable code while adhering to the latest React standards.
When to use it
Use this skill when writing or reviewing React components, particularly when dealing with complex state management or server/client component interactions.
When not to use it
This skill may not be suitable for extremely simple applications or when using older versions of React that do not support the discussed patterns.
What you can build with it
Building a New Component
When creating a new React component, use this skill to ensure you're following best practices for state management and composition.
Reviewing Existing Code
Utilize this skill to assess and improve the structure and performance of existing React components in your application.
Migrating to React 19
When updating your application to React 19, refer to this skill for guidance on adopting new features like form actions and server components.
How to install React Patterns
View source1. Install with the skills CLI
npx skills add affaan-m/ecc/react-patterns --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by affaan-mReact Patterns
Idiomatic React 18/19 patterns for building robust, accessible, performant component trees.
When to Activate
- Writing or modifying React function components, custom hooks, or component trees
- Reviewing JSX/TSX files
- Designing state shape or component composition
- Migrating class components or older
forwardRef/useEffect-heavy code - Choosing between local state, lifted state, context, and external stores
- Working with Server Components / Client Components (Next.js App Router, RSC)
- Implementing forms with React 19 actions or controlled inputs
- Wiring data fetching with TanStack Query / SWR / RSC
Core Principles
1. Render is a Pure Function of Props and State
// Good: derive during render
function Cart({ items }: { items: CartItem[] }) {
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
return <span>{formatMoney(total)}</span>;
}
// Bad: derived state stored separately
function Cart({ items }: { items: CartItem[] }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
return <span>{formatMoney(total)}</span>;
}
Derived state in useEffect adds a render cycle, can desync, and obscures the data flow.
2. Side Effects Outside Render
Effects, mutations, network calls, and subscriptions live in event handlers or useEffect — never in the render body.
3. Composition Over Inheritance
React has no inheritance model for components. Compose with children, render props, or component props.
Hooks Discipline
See rules/react/hooks.md for the full ruleset. Highlights:
- Top-level only, never conditional
- Cleanup every subscription, interval, listener
- Functional updater (
setX(prev => prev + 1)) when new state depends on old - Default position: do not memoize — add
useMemo/useCallbackonly when a profiler or a dependency chain proves it matters - Extract a custom hook only when the same hook sequence appears in 2+ components
State Location Decision Tree
Used by one component?
-> useState inside it
Used by parent + a few descendants?
-> lift to nearest common ancestor
Used across distant branches AND low-frequency reads (theme, auth, locale)?
-> React Context
High-frequency updates shared across the tree?
-> external store (Zustand, Jotai, Redux Toolkit)
Derived from a server?
-> server-state library (TanStack Query, SWR, RSC fetch)
Most pages do not need context or a global store. Resist abstraction until duplicated lifting becomes painful.
Server / Client Components (RSC)
// Server Component - default, async, never ships JS for itself
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) notFound();
return <ProductView product={product} />;
}
// Client Component - opt in with "use client"
"use client";
export function AddToCartButton({ productId }: { productId: string }) {
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending}
onClick={() => startTransition(() => addToCart(productId))}
>
{pending ? "Adding..." : "Add to cart"}
</button>
);
}
Boundaries:
- Server -> Client: pass serializable props or
children - Client -> Server: invoke Server Actions via
<form action={...}>or imperatively from event handlers - Never
importa Server Component from a Client Component file — compose them viachildreninstead
Suspense + Error Boundaries
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<UserSkeleton />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
- Place Suspense boundaries close to the data, not at the route root — progressively reveal content
- Error Boundary remains a class API; use
react-error-boundaryfor a hook-friendly wrapper - A boundary catches errors thrown during render, lifecycle, and constructors of its children — NOT in event handlers or async code
Forms
React 19 form actions (preferred for new code)
React 19+: This example uses
useActionState. For React 18, useuseFormStatefromreact-dominstead.
"use client";
import { useActionState } from "react";
const initial = { error: null as string | null };
async function updateUserAction(_prev: typeof initial, formData: FormData) {
"use server";
const parsed = UserSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: "Invalid input" };
await db.user.update({ where: { id: parsed.data.id }, data: parsed.data });
return { error: null };
}
export function UserForm() {
const [state, formAction, pending] = useActionState(updateUserAction, initial);
return (
<form action={formAction}>
<input name="name" required />
<button type="submit" disabled={pending}>Save</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
Controlled inputs
Use controlled when the value drives other UI, formats on every keystroke, or implements real-time validation.
Complex forms
For multi-step forms, dynamic field arrays, or cross-field validation: use a library (React Hook Form, TanStack Form). Roll-your-own state management for forms past trivial complexity is a maintenance trap.
Data Fetching Decision Matrix
| Need | Tool |
|---|---|
| Per-request data in Next.js App Router | RSC await fetch() |
| Client-side cache + mutations + invalidation | TanStack Query |
| Lightweight client cache + revalidation | SWR |
| Real-time subscriptions | Server-Sent Events, WebSockets, or the lib's subscription API |
| One-off fire-and-forget | fetch() in an event handler |
Avoid useEffect + fetch for application data — race conditions, no cache, no retry, no Suspense integration.
Composition Recipes
Slot via children
<Layout>
<Header />
<Main>{content}</Main>
</Layout>
Named slots
<Page header={<Nav />} sidebar={<Filters />}>
<Results />
</Page>
Compound components (shared state via Context)
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile"><Profile /></Tabs.Panel>
<Tabs.Panel value="settings"><Settings /></Tabs.Panel>
</Tabs>
Render prop / function-as-child
Useful when the parent needs to pass parameters to the rendered output:
<DataLoader id={id}>
{({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>
Modern alternative: a hook (useData(id)) returning the same shape — usually cleaner.
Performance
When React.memo Actually Helps
Wrap a component in React.memo only when:
- It re-renders frequently
- Its props are usually the same between renders
- Its render is measurably expensive
React.memo adds an equality check on every render. If props differ on most renders, the check is pure overhead.
Avoiding Render Cascades
- Lift state down rather than up where possible
- Split context: one context per concern, so a change to
themeContextdoes not re-render auth consumers - Use
useSyncExternalStorefor external state libraries — required for safe concurrent rendering
Lists
- Provide stable
keyprops (database id, not array index) - Virtualize long lists with
@tanstack/react-virtualorreact-windowonce visible item count exceeds ~50 with non-trivial rows
Accessibility-First Composition
- Always render semantic HTML (
<button>,<a>,<nav>,<main>) before reaching forroleattributes - Every interactive element must be reachable by keyboard
- Form inputs need labels —
<label htmlFor>oraria-labelif visually labeled by an icon - Manage focus on route changes and modal open/close
- Run
axein component tests (see skills/react-testing) - Cross-link: skills/accessibility/SKILL.md covers WCAG criteria and pattern libraries
Routing
This skill is router-agnostic. The patterns above work with React Router, TanStack Router, Next.js App Router, Remix Router. Router-specific patterns (loaders, actions, nested layouts) follow the router's documentation — those are framework concerns layered on top of React core.
Out of Scope (Pointer Sections)
- Next.js specifics: App Router data loading, Route Handlers, Middleware, Parallel Routes — separate concern, use Next.js docs
- React Native: Platform-specific patterns differ enough to warrant a separate
react-native-patternsskill (not present yet) - Remix: Loader/action conventions overlap with RSC but follow Remix docs
Related
- Rules: rules/react/ — coding-style, hooks, patterns, security, testing
- Skills: react-performance for the Vercel-derived performance ruleset, frontend-patterns for cross-framework UI concerns, accessibility, angular-developer for framework comparison
- Agents:
react-reviewerfor code review,react-build-resolverfor build/bundler errors - Commands:
/react-review,/react-build,/react-test
Examples
Custom hook for debounced search
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState("");
const debounced = useDebounce(query, 300);
const { data } = useQuery({
queryKey: ["search", debounced],
queryFn: () => searchApi(debounced),
enabled: debounced.length > 0,
});
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results items={data ?? []} />
</>
);
}
Optimistic UI with React 19 useOptimistic
"use client";
import { useOptimistic } from "react";
export function MessageList({ messages }: { messages: Message[] }) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, newMessage],
);
async function send(formData: FormData) {
const text = String(formData.get("text"));
addOptimistic({ id: "pending", text, sender: "me" });
await saveMessage(text);
}
return (
<>
<ul>{optimistic.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
<form action={send}>
<input name="text" />
<button type="submit">Send</button>
</form>
</>
);
}
Splitting context to avoid render cascades
// Two contexts: one rarely changes, one frequently
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);
// A component that only consumes ThemeContext does NOT re-render when notifications change
Frequently asked questions about React Patterns
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
