---
name: react-19
description: >
React 19 component patterns for the admin dashboard and customer app.
Trigger: When creating React components, hooks, forms, data fetching with React Query, or client state with Zustand.
license: Apache-2.0
metadata:
author: gentleman-programming
version: '1.0'
scope: [admin, customer]
auto_invoke: 'Writing React components or hooks'
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---
## Core Rules
- **Functional components only** — no class components
- **Container / Presentational pattern** — containers handle data, presentationals render
- **Custom hooks** for all reusable logic — never inline in components
- **React Query** for all server state (fetching, caching, mutations)
- **Zustand** for client-side global state
- **Zod + React Hook Form** for all forms
- NEVER use `useEffect` for data fetching — use React Query
- NEVER use prop drilling beyond 2 levels — use context or Zustand
---
## Container / Presentational Pattern
```tsx
// users-list.container.tsx — handles data
export function UsersListContainer() {
const { data: users, isLoading, error } = useUsersQuery();
if (isLoading) return ;
if (error) return ;
if (!users?.length) return ;
return ;
}
// users-list.tsx — pure presentational, no data fetching
interface UsersListProps {
readonly users: User[];
}
export function UsersList({ users }: UsersListProps) {
return (
{users.map((user) => (
))}
);
}
```
---
## Feature Folder Structure
```
src/features/your-feature/
├── components/
│ ├── your-feature-list.container.tsx # Data-aware
│ ├── your-feature-list.tsx # Presentational
│ └── your-feature-card.tsx # Presentational
├── hooks/
│ ├── use-your-feature-query.ts # React Query
│ └── use-your-feature-mutation.ts # React Query mutation
├── stores/
│ └── your-feature.store.ts # Zustand (if client state needed)
├── api/
│ └── your-feature.api.ts # API call functions
└── types.ts # Feature types
```
---
## React Query — Server State
```typescript
// use-users-query.ts
import { useQuery } from '@tanstack/react-query';
import { fetchUsers } from '../api/users.api';
export function useUsersQuery(tenantId: string) {
return useQuery({
queryKey: ['users', tenantId],
queryFn: () => fetchUsers(tenantId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// use-create-user-mutation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useCreateUserMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUserApi,
onSuccess: (newUser) => {
queryClient.invalidateQueries({ queryKey: ['users', newUser.tenantId] });
},
});
}
```
Query key convention: `[domain, tenantId, ...filters]`
---
## Forms with React Hook Form + Zod
```tsx
// create-user-form.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod/v4';
const CreateUserSchema = z.object({
name: z.string().min(1, { error: 'Name is required' }),
email: z.email({ error: 'Invalid email' }),
role: z.enum(['admin', 'member', 'viewer']),
});
type CreateUserFormData = z.infer;
export function CreateUserForm() {
const { mutate: createUser, isPending } = useCreateUserMutation();
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(CreateUserSchema),
});
const onSubmit = (data: CreateUserFormData) => {
createUser(data);
};
return (
);
}
```
---
## Custom Hooks
```typescript
// use-toggle.ts
interface UseToggleReturn {
readonly isOpen: boolean;
readonly open: () => void;
readonly close: () => void;
readonly toggle: () => void;
}
export function useToggle(initialValue = false): UseToggleReturn {
const [isOpen, setIsOpen] = useState(initialValue);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const toggle = () => setIsOpen((prev) => !prev);
return { isOpen, open, close, toggle };
}
```
React 19 note: `useCallback` wrapping is no longer needed — React Compiler handles memoization automatically.
---
## Code Splitting (MANDATORY for heavy features)
```tsx
import { lazy, Suspense } from 'react';
// Route-based code splitting
const BookingsDashboard = lazy(
() => import('@/features/bookings/components/bookings-dashboard.container'),
);
function App() {
return (
}>
);
}
```
---
## Component Rules
- Keep components under **150 lines**
- Extract complex JSX into sub-components
- No `useEffect` for data fetching — use React Query
- No inline objects/arrays as props (creates new references every render)
- Use **semantic HTML** for accessibility
- Add ARIA labels to interactive elements
```tsx
// ✅ GOOD: Semantic + accessible
```
---
## Declarative Naming for Components
```tsx
// ❌ BAD: Imperative/complex conjugations
// ✅ GOOD: Simple, declarative
```
---
## Component File Structure
```tsx
// user-card.tsx
// 1. Imports
import type { User } from '../types';
import { useToggle } from '../hooks/use-toggle';
// 2. Types
interface UserCardProps {
readonly user: User;
readonly onEdit?: (user: User) => void;
}
// 3. Sub-components (only if very small and tightly coupled)
function UserAvatar({ src, alt }: { src: string; alt: string }) {
return ;
}
// 4. Main component (named export)
export function UserCard({ user, onEdit }: UserCardProps) {
const { isOpen, toggle } = useToggle();
return (