---
name: react-best-practices
description: Comprehensive React and Next.js best practices guide covering performance optimization, component architecture, shadcn/ui patterns, Motion animations, and modern React 19+ patterns. This skill should be used when writing, reviewing, or refactoring React/Next.js code. Triggers on tasks involving React components, Next.js pages, data fetching, UI components, animations, or code quality improvements.
license: MIT
---
# React Best Practices
Comprehensive guide for building modern React and Next.js applications. Covers performance optimization, component architecture, shadcn/ui patterns, Motion animations, accessibility, and React 19+ features.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Building UI with shadcn/ui components
- Adding animations and micro-interactions
- Reviewing code for quality and performance
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Component Architecture | CRITICAL | `arch-` |
| 2 | Eliminating Waterfalls | CRITICAL | `async-` |
| 3 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 4 | Server Components & Actions | HIGH | `server-` |
| 5 | shadcn/ui Patterns | HIGH | `shadcn-` |
| 6 | State Management | MEDIUM-HIGH | `state-` |
| 7 | Motion & Animations | MEDIUM | `motion-` |
| 8 | Re-render Optimization | MEDIUM | `rerender-` |
| 9 | Accessibility | MEDIUM | `a11y-` |
| 10 | TypeScript Patterns | MEDIUM | `ts-` |
---
## 1. Component Architecture (CRITICAL)
### Quick Reference
- `arch-functional-components` - Use functional components with hooks exclusively
- `arch-composition-over-inheritance` - Build on existing components, don't extend
- `arch-single-responsibility` - Each component should do one thing well
- `arch-presentational-container` - Separate UI from logic when beneficial
- `arch-colocation` - Keep related files together (component, styles, tests)
- `arch-avoid-prop-drilling` - Use Context or composition for deep props
### Key Principles
**Functional Components Only**
```typescript
// Correct: Functional component with hooks
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useUser(userId)
return
{user?.name}
}
// Incorrect: Class component
class UserProfile extends React.Component { /* ... */ }
```
**Composition Pattern**
```typescript
// Correct: Compose smaller components
function Card({ children }: { children: React.ReactNode }) {
return {children}
}
function CardHeader({ children }: { children: React.ReactNode }) {
return {children}
}
// Usage
Title
Content
```
**Avoid Prop Drilling**
```typescript
// Incorrect: Passing props through many levels
// Correct: Use Context for shared state
const UserContext = createContext(null)
function App() {
const user = useCurrentUser()
return (
)
}
```
---
## 2. Eliminating Waterfalls (CRITICAL)
### Quick Reference
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Handle partial dependencies correctly
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### Key Principles
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
**Parallel Data Fetching**
```typescript
// Incorrect: Sequential waterfalls
async function Page() {
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
return {/* render */}
}
// Correct: Parallel fetching
async function Page() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
return {/* render */}
}
```
**Strategic Suspense Boundaries**
```typescript
// Stream content as it becomes available
function Page() {
return (
)
}
```
---
## 3. Bundle Size Optimization (CRITICAL)
### Quick Reference
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### Key Principles
**Avoid Barrel File Imports**
```typescript
// Incorrect: Imports entire library
import { Button } from '@/components'
import { formatDate } from '@/utils'
// Correct: Direct imports enable tree-shaking
import { Button } from '@/components/ui/button'
import { formatDate } from '@/utils/date'
```
**Dynamic Imports**
```typescript
import dynamic from 'next/dynamic'
// Load only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => ,
ssr: false
})
function Dashboard({ showChart }) {
return showChart ? : null
}
```
---
## 4. Server Components & Actions (HIGH)
### Quick Reference
- `server-default-server` - Components are Server Components by default
- `server-use-client-boundary` - Add 'use client' only when needed
- `server-actions` - Use Server Actions for mutations
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-serialization` - Minimize data passed to client components
### Key Principles
**Server Components by Default**
```typescript
// Server Component (default) - can be async
async function ProductPage({ id }: { id: string }) {
const product = await db.product.findUnique({ where: { id } })
return
}
// Client Component - only when needed for interactivity
'use client'
function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition()
return (
)
}
```
**Server Actions**
```typescript
// actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({ data: { title, content } })
revalidatePath('/posts')
}
// Component usage
function CreatePostForm() {
return (
)
}
```
---
## 5. shadcn/ui Patterns (HIGH)
### Quick Reference
- `shadcn-composition` - Build on existing shadcn/ui primitives
- `shadcn-variants` - Use class-variance-authority for component variants
- `shadcn-theme-integration` - Use CSS custom properties for theming
- `shadcn-accessibility` - Leverage built-in accessibility from Radix
- `shadcn-customization` - Modify copied components, don't wrap excessively
### Core Principles
shadcn/ui is built around:
- **Open Code**: Components are copied into your project, fully customizable
- **Composition**: Every component uses a common, composable interface
- **Beautiful Defaults**: Carefully chosen default styles
- **Accessibility by Default**: Built on Radix UI primitives
### Component Installation
```bash
# Add components as needed
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
```
### Building Custom Components
**Composition Over Creation**
```typescript
// Correct: Build on existing primitives
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface ProductCardProps {
product: Product
onSelect?: () => void
}
function ProductCard({ product, onSelect }: ProductCardProps) {
return (
{product.name}
{product.isNew && New}
{product.description}
${product.price}
)
}
```
**Using Variants with CVA**
```typescript
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const statusBadgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
{
variants: {
status: {
pending: 'bg-yellow-100 text-yellow-800',
active: 'bg-green-100 text-green-800',
inactive: 'bg-gray-100 text-gray-800',
error: 'bg-red-100 text-red-800',
},
},
defaultVariants: {
status: 'pending',
},
}
)
interface StatusBadgeProps
extends React.HTMLAttributes,
VariantProps {
label: string
}
function StatusBadge({ status, label, className, ...props }: StatusBadgeProps) {
return (
{label}
)
}
```
### Common shadcn/ui Components
**Forms with React Hook Form + Zod**
```typescript
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
function LoginForm() {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
})
function onSubmit(values: z.infer) {
console.log(values)
}
return (
)
}
```
**Dialog/Modal Pattern**
```typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
function ConfirmDialog({
onConfirm,
title,
description
}: {
onConfirm: () => void
title: string
description: string
}) {
return (
)
}
```
**Data Table with Tanstack Table**
```typescript
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
interface DataTableProps {
columns: ColumnDef[]
data: TData[]
}
function DataTable({
columns,
data,
}: DataTableProps) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => (
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
))}
))}
{table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
))}
))}
)
}
```
---
## 6. State Management (MEDIUM-HIGH)
### Quick Reference
- `state-local-first` - Use useState/useReducer for local state
- `state-context-static` - Use Context for infrequently changing data
- `state-derived-compute` - Compute derived values, don't store them
- `state-url-state` - Use URL for shareable/bookmarkable state
- `state-server-state` - Use SWR/TanStack Query for server state
### Key Principles
**Avoid Derived State**
```typescript
// Incorrect: Storing derived state
function ProductList({ products }) {
const [items, setItems] = useState(products)
const [count, setCount] = useState(products.length) // Derived!
// Bug: count can get out of sync with items
}
// Correct: Compute derived values
function ProductList({ products }) {
const [items, setItems] = useState(products)
const count = items.length // Always in sync
}
```
**URL State for Filters/Pagination**
```typescript
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
function ProductFilters() {
const searchParams = useSearchParams()
const router = useRouter()
const category = searchParams.get('category') || 'all'
function setCategory(newCategory: string) {
const params = new URLSearchParams(searchParams)
params.set('category', newCategory)
router.push(`?${params.toString()}`)
}
return (
)
}
```
---
## 7. Motion & Animations (MEDIUM)
### Quick Reference
- `motion-purposeful` - Animations should enhance UX, not distract
- `motion-performance` - Use transform/opacity, avoid layout triggers
- `motion-reduced-motion` - Respect prefers-reduced-motion
- `motion-layout-id` - Use layoutId for shared element transitions
- `motion-exit-animations` - Use AnimatePresence for exit animations
- `motion-variants` - Define reusable animation states
### Installation
```bash
npm install motion
```
### Core Principles
Motion (formerly Framer Motion) provides declarative animations that enhance user experience.
**Basic Animations**
```typescript
'use client'
import { motion } from 'motion/react'
function FadeInCard({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
**Interaction States**
```typescript
function InteractiveButton({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
**Exit Animations with AnimatePresence**
```typescript
import { motion, AnimatePresence } from 'motion/react'
function NotificationList({ notifications }: { notifications: Notification[] }) {
return (
{notifications.map((notification) => (
))}
)
}
```
**Shared Element Transitions**
```typescript
function ProductGrid({ products }: { products: Product[] }) {
const [selected, setSelected] = useState(null)
return (
<>
{products.map((product) => (
setSelected(product)}
className="cursor-pointer"
>
))}
{selected && (
setSelected(null)} />
)}
>
)
}
```
**Reusable Variants**
```typescript
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
}
const staggerContainer = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
}
function StaggeredList({ items }: { items: string[] }) {
return (
{items.map((item, i) => (
{item}
))}
)
}
```
**Scroll-Triggered Animations**
```typescript
import { motion, useInView } from 'motion/react'
import { useRef } from 'react'
function ScrollReveal({ children }: { children: React.ReactNode }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-100px' })
return (
{children}
)
}
```
**Respecting Reduced Motion**
```typescript
import { motion, useReducedMotion } from 'motion/react'
function AccessibleAnimation({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion()
return (
{children}
)
}
```
### Performance Tips
- Use `transform` and `opacity` for smooth 60fps animations
- Set `willChange` prop for complex animations
- Keep exit animations short (under 300ms)
- Use `useInView` to lazy-load animations
- Avoid animating `width`, `height`, `top`, `left` directly
---
## 8. Re-render Optimization (MEDIUM)
### Quick Reference
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-usememo` - Memoize expensive calculations
- `rerender-usecallback` - Stabilize callback references
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-transitions` - Use startTransition for non-urgent updates
### Key Principles
**Memoization for Expensive Components**
```typescript
import { memo } from 'react'
const ExpensiveList = memo(function ExpensiveList({
items
}: {
items: Item[]
}) {
return (
{items.map(item => (
- {/* expensive render */}
))}
)
})
```
**Stable Callbacks**
```typescript
function Parent() {
const [count, setCount] = useState(0)
// Stable reference - won't cause child re-renders
const handleClick = useCallback(() => {
setCount(c => c + 1)
}, [])
return
}
```
**Non-Urgent Updates with Transitions**
```typescript
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent) {
setQuery(e.target.value) // Urgent: update input immediately
startTransition(() => {
setResults(filterResults(e.target.value)) // Non-urgent: can be interrupted
})
}
return (
<>
{isPending && }
>
)
}
```
---
## 9. Accessibility (MEDIUM)
### Quick Reference
- `a11y-semantic-html` - Use correct HTML elements
- `a11y-keyboard-nav` - Ensure keyboard navigability
- `a11y-aria-labels` - Add descriptive labels for screen readers
- `a11y-focus-management` - Manage focus in modals and dynamic content
- `a11y-color-contrast` - Ensure sufficient color contrast
### Key Principles
**Semantic HTML**
```typescript
// Incorrect: div soup
Click me
// Correct: semantic button
```
**Focus Management in Modals**
```typescript
function Modal({ isOpen, onClose, children }) {
const closeButtonRef = useRef(null)
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus()
}
}, [isOpen])
return (
)
}
```
**Skip Links**
```typescript
function Layout({ children }) {
return (
<>
Skip to main content
{children}
>
)
}
```
---
## 10. TypeScript Patterns (MEDIUM)
### Quick Reference
- `ts-strict-mode` - Enable strict TypeScript configuration
- `ts-component-props` - Define explicit prop interfaces
- `ts-generics` - Use generics for reusable components
- `ts-discriminated-unions` - Use for state machines
- `ts-infer-when-possible` - Let TypeScript infer when obvious
### Key Principles
**Component Props**
```typescript
interface ButtonProps extends React.ButtonHTMLAttributes {
variant?: 'default' | 'destructive' | 'outline'
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
}
function Button({
variant = 'default',
size = 'md',
isLoading,
children,
disabled,
...props
}: ButtonProps) {
return (
)
}
```
**Discriminated Unions for State**
```typescript
type AsyncState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function useAsync(asyncFn: () => Promise) {
const [state, setState] = useState>({ status: 'idle' })
// TypeScript knows exact shape based on status
if (state.status === 'success') {
return state.data // TypeScript knows data exists
}
}
```
**Generic Components**
```typescript
interface SelectProps {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
getValue: (option: T) => string
}
function Select({ options, value, onChange, getLabel, getValue }: SelectProps) {
return (
)
}
```
---
## React 19+ Features
### New Hooks
**useActionState** - Form state management
```typescript
'use client'
import { useActionState } from 'react'
function SubscribeForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const email = formData.get('email')
const result = await subscribe(email)
return result
},
null
)
return (
)
}
```
**useOptimistic** - Optimistic UI updates
```typescript
'use client'
import { useOptimistic } from 'react'
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function addTodo(formData: FormData) {
const title = formData.get('title') as string
const newTodo = { id: crypto.randomUUID(), title, completed: false }
addOptimisticTodo(newTodo) // Immediately show in UI
await createTodo(title) // Then persist to server
}
return (
<>
{optimisticTodos.map(todo => (
- {todo.title}
))}
>
)
}
```
**use** - Async resource reading
```typescript
import { use, Suspense } from 'react'
async function fetchUser(id: string): Promise {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
function UserProfile({ userPromise }: { userPromise: Promise }) {
const user = use(userPromise) // Suspends until resolved
return {user.name}
}
function Page({ userId }: { userId: string }) {
const userPromise = fetchUser(userId)
return (
}>
)
}
```
---
## Project Structure
```
src/
├── app/ # Next.js App Router
│ ├── layout.tsx
│ ├── page.tsx
│ └── (routes)/
├── components/
│ ├── ui/ # shadcn/ui components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── ...
│ └── features/ # Feature-specific components
│ ├── auth/
│ └── dashboard/
├── hooks/ # Custom hooks
├── lib/ # Utilities
│ ├── utils.ts # cn() helper, etc.
│ └── validations.ts # Zod schemas
├── actions/ # Server Actions
└── types/ # TypeScript types
```
---
## References
- [React Documentation](https://react.dev)
- [Next.js Documentation](https://nextjs.org)
- [shadcn/ui Documentation](https://ui.shadcn.com)
- [Motion Documentation](https://motion.dev)
- [Radix UI Primitives](https://radix-ui.com)
- [TanStack Query](https://tanstack.com/query)
- [Tailwind CSS](https://tailwindcss.com)