| name | react-development |
| description | Builds portable, testable React applications using Container/View separation, dependency injection, and framework-agnostic architecture. Use when creating or refactoring React components, wiring data fetching with React Query, parsing URL state with Zod, handling forms with react-hook-form, or when components mix framework imports (next/navigation, useRouter, useParams) with presentation. Use when output must be Storybook-friendly, accessible, and survive a framework migration. |
| version | 1.3.0 |
| libraries | ["@tanstack/react-query","react-hook-form","zod","msw","class-variance-authority","nuqs"] |
React Development
Overview
Build frontend applications that are portable, testable, and user-focused. The principle: frameworks are adapters, not architectures. Your business logic and presentation should work with any React framework. Container/View patterns keep rendering portable, dependency injection keeps side effects mockable, and Storybook-first development catches UX issues before backends exist. User experience wins over developer convenience.
The payoff: presentational code renders in Storybook with no framework, tests mock handlers instead of routers, and a framework migration only rewrites the thin Container layer.
When to Use
- Creating new React components, pages, or features
- Refactoring components that mix framework imports with rendering
- Wiring data fetching, caching, and mutations with React Query
- Parsing URL/search-param state at a boundary with Zod or nuqs
- Building forms with react-hook-form + Zod validation
- Setting up ESLint boundary rules to enforce portability
When NOT to use: for a throwaway spike or design exploration where the architecture is the question, prototype first. For pure visual/spacing/typography decisions, use ui-design-principles. For end-to-end flow demos (signup/checkout), use storybook-journeys.
Related: storybook-journeys (page-level flow stories), ui-design-principles (visual design), agent-browser (browser verification), testing-strategy (test pyramid), validation-boundary (Zod at boundaries).
Deep-Dive References
This SKILL.md is the focused core. Detailed patterns live one level deep:
- PATTERNS.md: React Query (keys, mutations, optimistic, staleTime), URL state with Zod, React 19 (
useTransition, useOptimistic, useActionState), forms, custom hooks, folder structure, ESLint boundaries.
- INTERACTIONS.md: Web Interface Guidelines: keyboard, targets, forms, feedback, animation, layout, content/accessibility, performance, theming, hydration, design (MUST/SHOULD/NEVER rules).
- STORYBOOK.md: Storybook-first development: stories per component, MSW for data-fetching states, play functions, testing philosophy.
Critical Rules
| Rule | Enforcement |
|---|
No any, no as | Type-safe solutions always exist |
| No framework imports in components/hooks/lib/queries | ESLint boundary rules (see PATTERNS.md) |
| Test what pays | Domain logic, critical flows, not snapshots |
| Accessibility required | ~15% of users have disabilities; legal requirement in many regions |
| Handle all states | Loading, error, empty, offline |
Container/View Pattern
Separate data orchestration (Container) from presentation (View). The View is pure, prop-driven, and framework-free; the Container is the thin boundary that touches the framework.
WRONG: Coupled Component
function UserProfile() {
const { id } = useParams();
const router = useRouter();
const { data, isLoading } = useQuery(['user', id], () => fetchUser(id));
if (isLoading) return <Spinner />;
return (
<div>
<h1>{data.name}</h1>
<button onClick={() => router.push(`/users/${id}/edit`)}>Edit</button>
</div>
);
}
CORRECT: Container/View Split
type UserProfileViewProps = {
user: User;
handlers: {
onEdit: () => void;
onDelete: () => void;
};
};
export function UserProfileView({ user, handlers }: UserProfileViewProps) {
return (
<div>
<h1>{user.name}</h1>
<button onClick={handlers.onEdit}>Edit</button>
<button onClick={handlers.onDelete}>Delete</button>
</div>
);
}
'use client';
import { useParams, useRouter } from 'next/navigation';
export function UserProfileContainer() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const { data: user, isLoading, error } = useUserQuery(id);
const handlers = {
onEdit: () => router.push(`/users/${id}/edit`),
onDelete: () => deleteUserMutation.mutate(id),
};
if (isLoading) return <UserProfileSkeleton />;
if (error) return <ErrorState error={error} />;
if (!user) return <EmptyState message="User not found" />;
return <UserProfileView user={user} handlers={handlers} />;
}
Why This Matters
| Benefit | How |
|---|
| Storybook works | View has no framework imports |
| Testing is easy | Mock handlers, not routers |
| Framework migration | Only rewrite Containers |
| Type safety | Props are explicit contracts |
Dependency Injection: handlers vs deps
Two distinct prop types for two distinct purposes. Both keep the View pure.
| Prop Type | Purpose | Example |
|---|
handlers | User-initiated actions | onEdit, onDelete, onSubmit |
deps | Platform/environment capabilities | getInitialValue, storage, analytics |
type ProductCardProps = {
product: Product;
handlers: {
onAddToCart: () => void;
onViewDetails: () => void;
};
deps?: {
trackEvent?: (name: string) => void;
};
};
function ProductCard({ product, handlers, deps }: ProductCardProps) {
const handleAddToCart = () => {
deps?.trackEvent?.('add_to_cart');
handlers.onAddToCart();
};
return (
<article>
<h2>{product.name}</h2>
<button onClick={handleAddToCart}>Add to Cart</button>
<button onClick={handlers.onViewDetails}>View Details</button>
</article>
);
}
State Decision Guide
Choose the simplest mechanism that fits the data's lifetime and scope. Full React 19 patterns in PATTERNS.md.
| Need | Solution |
|---|
| Server state (fetch, cache, sync) | React Query |
| Non-blocking UI updates | useTransition |
| Optimistic UI | useOptimistic |
| Form submission state | useActionState |
| Local component state | useState / useReducer |
| Shareable UI (filters, tabs, pagination) | URL state (Zod / nuqs) |
| Shared within a feature subtree | React Context |
| Proven cross-tree sync | Zustand (only after measuring) |
Avoid prop drilling deeper than 3 levels. If you pass props through components that don't use them, introduce context or restructure the tree.
Accessibility (WCAG 2.1 AA baseline)
| Requirement | Implementation |
|---|
| Keyboard nav | Tab, Enter, Escape, Arrow keys per WAI-ARIA APG |
| Focus management | Trap in modals, restore on close |
| ARIA labels | When semantic HTML isn't enough |
| Color contrast | WCAG AA 4.5:1 for text; prefer APCA |
| Error association | aria-invalid, aria-describedby |
| Loading states | aria-busy, polite aria-live announcements |
WRONG
<div onClick={handleAction}>Click me</div>
CORRECT
<button onClick={handleAction} aria-label="Add item to cart">
Add to Cart
</button>
Full interaction, animation, performance, and theming rules: INTERACTIONS.md.
Red Flags
- Framework imports (
next/navigation, next/router) inside components/, hooks/, lib/, or queries/
- A component both fetches data and renders presentation with no Container/View split
<div onClick> used for navigation or actions (use <a>/<Link> and <button>)
any or as casts to silence the type checker
- Missing loading, error, or empty states (blank screens on slow networks)
- Color as the sole indicator of state (red/green with no text or icon)
- Components over ~200 lines, or a single "kitchen sink" hook doing 20 things
transition: all or animating layout props (top, left, width, height)
- Query keys built inline as raw arrays instead of a key factory
Verification
Before shipping a React feature:
Integration