react-patterns

$npx mdskill add jezweb/claude-skills/react-patterns

Optimize React 19 components for performance and composition.

  • Eliminates waterfall requests and reduces re-renders in React apps.
  • Depends on Vite, Cloudflare Workers, and React 19 APIs.
  • Ranks 50+ rules by impact to prioritize critical fixes first.
  • Delivers a checklist for writing, reviewing, or refactoring components.

SKILL.md

.github/skills/react-patternsView on GitHub ↗
---
name: react-patterns
description: "React 19 performance patterns and composition architecture for Vite + Cloudflare projects. 50+ rules ranked by impact — eliminating waterfalls, bundle optimisation, re-render prevention, composition over boolean props, server/client boundaries, and React 19 APIs. Use when writing, reviewing, or refactoring React components. Triggers: 'react patterns', 'react review', 'react performance', 'optimise components', 'react best practices', 'composition patterns', 'why is it slow', 'reduce re-renders', 'fix waterfall'."
compatibility: claude-code-only
allowed-tools:
  - Read
  - Glob
  - Grep
---

# React Patterns

Performance and composition patterns for React 19 + Vite + Cloudflare Workers projects. Use as a checklist when writing new components, a review guide when auditing existing code, or a refactoring playbook when something feels slow or tangled.

Rules are ranked by impact. Fix CRITICAL issues before touching MEDIUM ones.

## When to Apply

- Writing new React components or pages
- Reviewing code for performance issues
- Refactoring components with too many props or re-renders
- Debugging "why is this slow?" or "why does this re-render?"
- Building reusable component libraries
- Code review before merging

## 1. Eliminating Waterfalls (CRITICAL)

Sequential async calls where they could be parallel. The #1 performance killer.

| Pattern | Problem | Fix |
|---------|---------|-----|
| **Await in sequence** | `const a = await getA(); const b = await getB();` | `const [a, b] = await Promise.all([getA(), getB()]);` |
| **Fetch in child** | Parent renders, then child fetches, then grandchild fetches | Hoist fetches to the highest common ancestor, pass data down |
| **Suspense cascade** | Multiple Suspense boundaries that resolve sequentially | One Suspense boundary wrapping all async siblings |
| **Await before branch** | `const data = await fetch(); if (condition) { use(data); }` | Move await inside the branch — don't fetch what you might not use |
| **Import then render** | `const Component = await import('./Heavy'); return <Component />` | Use `React.lazy()` + `<Suspense>` — renders fallback instantly |

**How to find them**: Search for `await` in components. Each `await` is a potential waterfall. If two awaits are independent, they should be parallel.

## 2. Bundle Size (CRITICAL)

Every KB the user downloads is a KB they wait for.

| Pattern | Problem | Fix |
|---------|---------|-----|
| **Barrel imports** | `import { Button } from '@/components'` pulls the entire barrel file | `import { Button } from '@/components/ui/button'` — direct import |
| **No code splitting** | Heavy component loaded on every page | `React.lazy(() => import('./HeavyComponent'))` + `<Suspense>` |
| **Third-party at load** | Analytics/tracking loaded before the app renders | Load after hydration: `useEffect(() => { import('./analytics') }, [])` |
| **Full library import** | `import _ from 'lodash'` (70KB) | `import debounce from 'lodash/debounce'` (1KB) |
| **Lucide tree-shaking** | `import * as Icons from 'lucide-react'` (all icons) | Explicit map: `import { Home, Settings } from 'lucide-react'` |
| **Duplicate React** | Library bundles its own React → "Cannot read properties of null" | `resolve.dedupe: ['react', 'react-dom']` in vite.config.ts |

**How to find them**: `npx vite-bundle-visualizer` — shows what's in your bundle.

## 3. Composition Architecture (HIGH)

How you structure components matters more than how you optimise them.

| Pattern | Problem | Fix |
|---------|---------|-----|
| **Boolean prop explosion** | `<Card isCompact isClickable showBorder hasIcon isLoading>` | Explicit variants: `<CompactCard>`, `<ClickableCard>` |
| **Compound components** | Complex component with 15 props | Split into `<Dialog>`, `<Dialog.Trigger>`, `<Dialog.Content>` with shared context |
| **renderX props** | `<Layout renderSidebar={...} renderHeader={...} renderFooter={...}>` | Use children + named slots: `<Layout><Sidebar /><Header /></Layout>` |
| **Lift state** | Sibling components can't share state | Move state to parent or context provider |
| **Provider implementation** | Consumer code knows about state management internals | Provider exposes interface `{ state, actions, meta }` — implementation hidden |
| **Inline components** | `function Parent() { function Child() { ... } return <Child /> }` | Define Child outside Parent — inline components remount on every render |

**The test**: If a component has more than 5 boolean props, it needs composition, not more props.

## 4. Re-render Prevention (MEDIUM)

Not all re-renders are bad. Only fix re-renders that cause visible jank or wasted computation.

| Pattern | Problem | Fix |
|---------|---------|-----|
| **Default object/array props** | `function Foo({ items = [] })` → new array ref every render | Hoist: `const DEFAULT = []; function Foo({ items = DEFAULT })` |
| **Derived state in effect** | `useEffect(() => setFiltered(items.filter(...)), [items])` | Derive during render: `const filtered = useMemo(() => items.filter(...), [items])` |
| **Object dependency** | `useEffect(() => {...}, [config])` fires every render if config is `{}` | Use primitive deps: `useEffect(() => {...}, [config.id, config.type])` |
| **Subscribe to unused state** | Component reads `{ user, theme, settings }` but only uses `user` | Split context or use selector: `useSyncExternalStore` |
| **State for transient values** | `const [mouseX, setMouseX] = useState(0)` on mousemove | Use `useRef` for values that change frequently but don't need re-render |
| **Inline callback props** | `<Button onClick={() => doThing(id)} />` — new function every render | `useCallback` or functional setState: `<Button onClick={handleClick} />` |

**How to find them**: React DevTools Profiler → "Why did this render?" or `<React.StrictMode>` double-renders in dev.

## 5. React 19 Specifics (MEDIUM)

Patterns that changed or are new in React 19.

| Pattern | Old (React 18) | New (React 19) |
|---------|---------------|----------------|
| **Form state** | `useFormState` | `useActionState` — renamed |
| **Ref forwarding** | `forwardRef((props, ref) => ...)` | `function Component({ ref, ...props })` — ref is a regular prop |
| **Context** | `useContext(MyContext)` | `use(MyContext)` — works in conditionals and loops |
| **Pending UI** | Manual loading state | `useTransition` + `startTransition` for non-urgent updates |
| **Route-level lazy** | Works with `createBrowserRouter` only | Still true — `<Route lazy={...}>` is silently ignored with `<BrowserRouter>` |
| **Optimistic updates** | Manual state management | `useOptimistic` hook |
| **Metadata** | Helmet or manual `<head>` management | `<title>`, `<meta>`, `<link>` in component JSX — hoisted to `<head>` automatically |

## 6. Rendering Performance (MEDIUM)

| Pattern | Problem | Fix |
|---------|---------|-----|
| **Layout shift on load** | Content jumps when async data arrives | Skeleton screens matching final layout dimensions |
| **Animate SVG directly** | Janky SVG animation | Wrap in `<div>`, animate the div instead |
| **Large list rendering** | 1000+ items in a table/list | `@tanstack/react-virtual` for virtualised rendering |
| **content-visibility** | Long scrollable content renders everything upfront | `content-visibility: auto` on off-screen sections |
| **Conditional render with &&** | `{count && <Items />}` renders `0` when count is 0 | Use ternary: `{count > 0 ? <Items /> : null}` |

## 7. Data Fetching (MEDIUM)

| Pattern | Problem | Fix |
|---------|---------|-----|
| **No deduplication** | Same data fetched by 3 components | TanStack Query or SWR — automatic dedup + caching |
| **Fetch on mount** | `useEffect(() => { fetch(...) }, [])` — waterfalls, no caching, no dedup | TanStack Query: `useQuery({ queryKey: ['users'], queryFn: fetchUsers })` |
| **No optimistic update** | User clicks save, waits 2 seconds, then sees change | `useMutation` with `onMutate` for instant visual feedback |
| **Stale closure in interval** | `setInterval` captures stale state | `useRef` for the interval ID and current values |
| **Polling without cleanup** | `setInterval` in useEffect without `clearInterval` | Return cleanup: `useEffect(() => { const id = setInterval(...); return () => clearInterval(id); })` |

## 8. Vite + Cloudflare Specifics (MEDIUM)

| Pattern | Problem | Fix |
|---------|---------|-----|
| **`import.meta.env` in Node scripts** | Undefined — only works in Vite-processed files | Use `loadEnv()` from vite |
| **React duplicate instance** | Library bundles its own React | `resolve.dedupe` + `optimizeDeps.include` in vite.config.ts |
| **Radix Select empty string** | `<SelectItem value="">` throws | Use sentinel: `<SelectItem value="__any__">` |
| **React Hook Form null** | `{...field}` passes null to Input | Spread manually: `value={field.value ?? ''}` |
| **Env vars at edge** | `process.env` doesn't exist in Workers | Use `c.env` (Hono context) or `import.meta.env` (Vite build-time) |

## Using as a Review Checklist

When reviewing code, go through categories 1-3 (CRITICAL + HIGH) for every PR. Categories 4-8 only when performance is a concern.

```
/react-patterns [file or component path]
```

Read the file, check against rules in priority order, report findings as:
```
file:line — [rule] description of issue
```

More from jezweb/claude-skills

SkillDescription
ai-image-generatorGenerate AI images using Gemini or GPT APIs directly. Covers model selection (Gemini for scenes; GPT Image 2 for text rendering, batch variations, multi-reference compositing; GPT Image 1.5 for transparent icons), the 5-part prompting framework, API calling patterns, multi-turn editing, and quality assurance. Produces photorealistic scenes, icons, illustrations, OG images, posters, infographics, and product shots. Use when building websites that need images, creating marketing assets, or generating visual content. Triggers: 'generate image', 'ai image', 'create hero image', 'make an icon', 'generate illustration', 'create og image', 'poster', 'infographic', 'image variations', 'gpt-image-2', 'ai art', 'image generation'.
app-docsGenerate complete user documentation for a web app with screenshots. Browses the app via browser automation, screenshots every screen, and produces a structured user guide with step-by-step instructions, annotated screenshots, workflow diagrams, and reference tables. Supports quick (key screens), standard (all pages), thorough (every state and flow), and exhaustive (publishable documentation suite). Triggers: 'document the app', 'user guide', 'app documentation', 'screenshot docs', 'generate user docs', 'help docs', 'how-to guide', 'write the docs'.
aussie-business-englishAustralian business English for professional writing — warm, direct, EN-AU spelling (colour, organise, centre), no filler words. Use whenever the user is writing for an Australian audience: emails, chat messages, proposals, client communications, blog posts, web copy, or any business writing. Apply to drafting, editing, and tone-checking professional text.
award-applicationWrite compelling award submissions, grant applications, and competition entries. Maps achievements to selection criteria using evidence-based narratives. Use whenever the user is applying for a business award (Telstra, chamber of commerce, industry awards), a grant, a competition, or any submission demonstrating merit against defined criteria.
cloudflare-apiHit the Cloudflare REST API directly for operations that wrangler and MCP can't handle well. Bulk DNS, custom hostnames, email routing, cache purge, WAF rules, redirect rules, zone settings, Worker routes, D1 cross-database queries, R2 bulk operations, KV bulk read/write, Vectorize queries, Queues, and fleet-wide resource audits. Produces curl commands or scripts. Triggers: 'cloudflare api', 'bulk dns', 'custom hostname', 'email routing', 'cache purge', 'waf rule', 'd1 query', 'r2 bucket', 'kv bulk', 'vectorize query', 'audit resources', 'fleet operation'.
cloudflare-worker-builderScaffold and deploy Cloudflare Workers with Hono routing, Vite plugin, and Static Assets. Describe project, scaffold structure, configure bindings, deploy. Use whenever the user wants to create a Worker project, set up Hono on Cloudflare, configure D1 / R2 / KV / Queues bindings, or troubleshoot Worker export syntax, API route conflicts, HMR issues, or deployment failures.
codex-reviewRun an independent code review using the OpenAI Codex CLI in headless mode. Gets a second opinion from a different model family (the current Codex models) on recent changes, a PR, a commit, or the whole app — covering bugs, regressions, security, data consistency, UX/state bugs, performance risks, and testing gaps. Saves a severity-prioritised report to .jez/reviews/. Triggers: 'codex review', 'review with codex', 'independent code review', 'what does codex think', 'get codex to review'.
color-paletteGenerate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a design system, wants Tailwind theme colours from a brand colour, or asks to check colour accessibility / contrast.
d1-drizzle-schemaGenerate Drizzle ORM schemas for Cloudflare D1 databases with correct D1-specific patterns. Produces schema files, migration commands, type exports, and DATABASE_SCHEMA.md documentation. Handles D1 quirks: foreign keys always enforced, no native BOOLEAN/DATETIME types, 100 bound parameter limit, JSON stored as TEXT. Use when creating a new database, adding tables, or scaffolding a D1 data layer.
d1-migrationCloudflare D1 migration workflow: generate with Drizzle, inspect SQL for gotchas, apply to local and remote, fix stuck migrations, handle partial failures. Use when running migrations, fixing migration errors, or setting up D1 schemas.