---
name: react-best-practices
description: "React 19+ best practices: Server Components, use() hook, Actions, hooks patterns, state management, and performance optimization. Use when building or reviewing React work: components, hooks, state, Server Components, Actions."
tags: [react, frontend, javascript, typescript, hooks, state-management]
version: "2025.1"
---

# React Best Practices (React 19+)

## Core Principles

React 19 introduces Server Components as default, the `use()` hook for promises and context,
Actions for form handling, and improved concurrent rendering. All new code should target
React 19+ patterns.

## Component Architecture

### Functional Components Only

```tsx
// Server Component (default - no directive needed)
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId);
  return (
    <section>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </section>
  );
}

// Client Component (explicit opt-in)
'use client';

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
```

### The use() Hook

```tsx
'use client';

import { use } from 'react';

function UserDetails({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <div>{user.name}</div>;
}

// Reading context with use()
function ThemeButton() {
  const theme = use(ThemeContext);
  return <button className={theme}>Click me</button>;
}
```

### Actions and Form Handling

```tsx
'use client';

import { useActionState, useOptimistic } from 'react';

function AddToCartForm({ itemId }: { itemId: string }) {
  const [error, submitAction, isPending] = useActionState(
    async (_prevState: string | null, formData: FormData) => {
      const result = await addToCart(itemId);
      if (result.error) return result.error;
      return null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <button type="submit" disabled={isPending}>
        {isPending ? 'Adding...' : 'Add to Cart'}
      </button>
      {error && <p className="error">{error}</p>}
    </form>
  );
}
```

### Optimistic Updates

```tsx
'use client';

import { useOptimistic } from 'react';

function MessageList({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    (state, newMessage: string) => [
      ...state,
      { id: 'temp', text: newMessage, sending: true },
    ]
  );

  async function sendMessage(formData: FormData) {
    const text = formData.get('message') as string;
    addOptimistic(text);
    await deliverMessage(text);
  }

  return (
    <>
      {optimisticMessages.map((msg) => (
        <div key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
          {msg.text}
        </div>
      ))}
      <form action={sendMessage}>
        <input name="message" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}
```

## State Management

### Local State

```tsx
// Simple state
const [value, setValue] = useState<string>('');

// Complex state with useReducer
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };

function reducer(state: { count: number }, action: Action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset': return { count: 0 };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
```

### Zustand (Recommended External State)

```tsx
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface AuthStore {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
}

const useAuthStore = create<AuthStore>()(
  devtools(
    persist(
      (set) => ({
        user: null,
        isAuthenticated: false,
        login: async (credentials) => {
          const user = await authApi.login(credentials);
          set({ user, isAuthenticated: true });
        },
        logout: () => set({ user: null, isAuthenticated: false }),
      }),
      { name: 'auth-storage' }
    )
  )
);
```

## Performance

### React.memo and Callbacks

```tsx
import { memo, useCallback, useMemo } from 'react';

const ExpensiveList = memo(function ExpensiveList({ items, onSelect }: Props) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

function Parent() {
  const [filter, setFilter] = useState('');
  const [items] = useState(initialItems);

  const filteredItems = useMemo(
    () => items.filter((item) => item.name.includes(filter)),
    [items, filter]
  );

  const handleSelect = useCallback((id: string) => {
    console.log('Selected:', id);
  }, []);

  return <ExpensiveList items={filteredItems} onSelect={handleSelect} />;
}
```

### Code Splitting with React.lazy

```tsx
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <Suspense fallback={<div>Loading chart...</div>}>
      <HeavyChart />
    </Suspense>
  );
}
```

### Error Boundaries

```tsx
'use client';

import { Component, type ReactNode } from 'react';

interface Props { children: ReactNode; fallback: ReactNode; }
interface State { hasError: boolean; }

class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false };

  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    reportError(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}
```

## Do's

- Use Server Components by default; add 'use client' only when needed
- Use `useActionState` for form submissions instead of manual state
- Use `useOptimistic` for instant UI feedback
- Colocate state as close to where it is used as possible
- Use Zustand or Jotai for global client state (not Redux for new projects)
- Use React.memo only after measuring a performance problem
- Use Suspense boundaries at meaningful loading boundaries
- Use TypeScript strict mode for all React projects

## Don'ts

- Do not use class components for new code
- Do not use `useEffect` for data fetching (use Server Components or React Query)
- Do not put 'use client' at the top of every file
- Do not use `useEffect` to sync state derived from props (use `useMemo`)
- Do not create god components with 200+ lines; decompose
- Do not use `any` type; define proper interfaces
- Do not mutate state directly; always use setter functions
- Do not wrap everything in `React.memo`; profile first

## Troubleshooting

| Problem | Cause | Solution |
|---------|-------|----------|
| Hydration mismatch | Server/client render differ | Ensure consistent rendering; use `suppressHydrationWarning` only for dates/times |
| Infinite re-render | State update in render body | Move state updates to event handlers or effects |
| Stale closure | Missing dependency in useCallback | Add all dependencies or use useRef for mutable values |
| "Cannot use hooks in Server Component" | Hook used without 'use client' | Add 'use client' directive or lift state to a Client Component wrapper |
| Slow renders | Expensive computation in body | Use useMemo, split components, or move to Server Components |
