Claude
Skills
Sign in
Back

react-development

Included with Lifetime
$97 forever

Modern React architecture patterns and web interface guidelines. Container/View split, framework adapters, React Query, dependency injection, Storybook-first development, accessibility, interactions, animations, performance. User experience over developer convenience.

Web Dev

What this skill does


# React Development

Build frontend applications that are portable, testable, and user-focused.

## Core Principle

**Frameworks are adapters, not architectures.** Your business logic should work with any React framework. Container/View patterns keep rendering portable. Storybook-first development catches UX issues before backends exist.

## Critical Rules

| Rule | Enforcement |
|------|-------------|
| No `any`, no `as` | Type-safe solutions always exist |
| No framework imports in components | ESLint boundary rules |
| Test what pays | Domain logic, critical flows, not snapshots |
| Accessibility required | 15% of users have disabilities |
| Handle all states | Loading, error, empty, offline |

---

## Container/View Pattern

Separate data orchestration (Container) from presentation (View).

### WRONG - Coupled Component

```tsx
// UserProfile.tsx - Does everything, untestable
function UserProfile() {
  const { id } = useParams();  // Framework-specific
  const router = useRouter();   // Framework-specific
  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

```tsx
// UserProfileView.tsx - Pure presentation, portable
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>
  );
}

// UserProfileContainer.tsx - Framework boundary
'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 different purposes:

| Prop Type | Purpose | Example |
|-----------|---------|---------|
| `handlers` | User-initiated actions | `onEdit`, `onDelete`, `onSubmit` |
| `deps` | Platform/environment capabilities | `getInitialValue`, `storage`, `analytics` |

```tsx
type ProductCardProps = {
  product: Product;
  handlers: {
    onAddToCart: () => void;
    onViewDetails: () => void;
  };
  deps?: {
    trackEvent?: (name: string) => void;  // Optional analytics
  };
};

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>
  );
}
```

---

## React Query Patterns

### Key Factories

Organize query keys systematically:

```typescript
// queries/userKeys.ts
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

// Usage
const { data } = useQuery({
  queryKey: userKeys.detail(userId),
  queryFn: () => fetchUser(userId),
});

// Invalidation
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
```

### Mutation Pattern

```typescript
export function useUpdateUserMutation() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: UpdateUserData) => updateUser(data),
    onSuccess: (updatedUser) => {
      // Update cache directly
      queryClient.setQueryData(
        userKeys.detail(updatedUser.id),
        updatedUser
      );
      // Invalidate list views
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}
```

### Optimistic Updates

```typescript
useMutation({
  mutationFn: toggleLike,
  onMutate: async (postId) => {
    await queryClient.cancelQueries({ queryKey: postKeys.detail(postId) });
    const previous = queryClient.getQueryData(postKeys.detail(postId));
    queryClient.setQueryData(postKeys.detail(postId), (old: Post) => ({
      ...old,
      isLiked: !old.isLiked,
      likeCount: old.isLiked ? old.likeCount - 1 : old.likeCount + 1,
    }));
    return { previous };
  },
  onError: (_err, postId, context) => {
    queryClient.setQueryData(postKeys.detail(postId), context?.previous);
  },
});
```

### Loading State Policies

Choose strategically based on UX needs:

| staleTime | Behavior | Use When |
|-----------|----------|----------|
| `0` (default) | Always refetch | Real-time data (stock prices) |
| `30_000` | Cache 30s | User profiles, product details |
| `Infinity` | Never refetch | Static reference data |

```typescript
const { data } = useQuery({
  queryKey: userKeys.detail(userId),
  queryFn: () => fetchUser(userId),
  staleTime: 30_000,  // 30 seconds
  gcTime: 5 * 60 * 1000,  // Keep in cache 5 minutes
});
```

---

## URL State with Zod

Parse URL parameters once at the boundary, pass typed data down:

```typescript
// lib/url-state.ts
import { z } from 'zod';

export const searchParamsSchema = z.object({
  page: z.coerce.number().positive().default(1),
  sort: z.enum(['name', 'date', 'price']).default('name'),
  direction: z.enum(['asc', 'desc']).default('asc'),
  search: z.string().optional(),
});

export type SearchParams = z.infer<typeof searchParamsSchema>;

export function parseSearchParams(params: URLSearchParams): SearchParams {
  const raw = Object.fromEntries(params.entries());
  const result = searchParamsSchema.safeParse(raw);
  return result.success ? result.data : searchParamsSchema.parse({});
}
```

```tsx
// Container parses once
function ProductListContainer() {
  const searchParams = useSearchParams();
  const parsed = parseSearchParams(searchParams);

  return <ProductListView filters={parsed} />;
}
```

---

## React 19 Patterns

### useTransition for Non-Blocking Updates

```tsx
function ProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value: string) => {
    setSearchTerm(value);  // High priority - update input immediately
    startTransition(() => {
      updateFilters({ search: value });  // Low priority - won't block typing
    });
  };

  return (
    <div className="relative">
      <input
        value={searchTerm}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search..."
      />
      {isPending && <Spinner className="absolute right-2 top-2" size="sm" />}
    </div>
  );
}
```

### useOptimistic for Instant Feedback

```tsx
function LikeButton({ postId, initialLikes, isLiked }: LikeButtonProps) {
  const [optimisticState, addOptimistic] = useOptimistic(
    { likes: initialLikes, isLiked },
    (state, action: 'like' | 'unlike') => ({
      likes: action === 'like' ? state.likes + 1 : Math.max(0, state.likes - 1),
      isLiked: action === 'like',
    })
  );

  const toggleLike = async () => {
    const action = optimisticState.isLiked ? 'unl

Related in Web Dev