react-patterns

SKILL.md

React Development Patterns

Expert guide for building modern React 19 applications with new concurrent features, Server Components, Actions, and advanced patterns.

When to Use

  • Building React 19 components with TypeScript/JavaScript
  • Managing component state with useState and useReducer
  • Handling side effects with useEffect
  • Optimizing performance with useMemo and useCallback
  • Creating custom hooks for reusable logic
  • Implementing component composition patterns
  • Working with refs using useRef
  • Using React 19's new features (use(), useOptimistic, useFormStatus)
  • Implementing Server Components and Actions
  • Working with Suspense and concurrent rendering
  • Building forms with new form hooks

Core Hooks Patterns

useState - State Management

Basic state declaration and updates:

import { useState } from 'react';

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

State with initializer function (expensive computation):

const [state, setState] = useState(() => {
  const initialState = computeExpensiveValue();
  return initialState;
});

Multiple state variables:

function UserProfile() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');
  
  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input type="number" value={age} onChange={e => setAge(Number(e.target.value))} />
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

useEffect - Side Effects

Basic effect with cleanup:

import { useEffect } from 'react';

function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    
    // Cleanup function
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // Dependency array
  
  return <div>Connected to {roomId}</div>;
}

Effect with multiple dependencies:

function ChatRoom({ roomId, serverUrl }: { roomId: string; serverUrl: string }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // Re-run when either changes
  
  return <h1>Welcome to {roomId}</h1>;
}

Effect for subscriptions:

function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    
    function handleOffline() {
      setIsOnline(false);
    }
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []); // Empty array = run once on mount
  
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

useRef - Persistent References

Storing mutable values without re-renders:

import { useRef } from 'react';

function Timer() {
  const intervalRef = useRef<NodeJS.Timeout | null>(null);
  
  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);
  };
  
  const stopTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
  };
  
  return (
    <>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </>
  );
}

DOM element references:

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const focusInput = () => {
    inputRef.current?.focus();
  };
  
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}

Custom Hooks Pattern

Extract reusable logic into custom hooks:

// useOnlineStatus.ts
import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    
    function handleOffline() {
      setIsOnline(false);
    }
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  return isOnline;
}

// Usage in components
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();
  return (
    <button disabled={!isOnline}>
      {isOnline ? 'Save' : 'Reconnecting...'}
    </button>
  );
}

Custom hook with parameters:

// useChatRoom.ts
import { useEffect } from 'react';

interface ChatOptions {
  serverUrl: string;
  roomId: string;
}

export function useChatRoom({ serverUrl, roomId }: ChatOptions) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    
    return () => connection.disconnect();
  }, [serverUrl, roomId]);
}

// Usage
function ChatRoom({ roomId }: { roomId: string }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  
  useChatRoom({ serverUrl, roomId });
  
  return (
    <>
      <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      <h1>Welcome to {roomId}</h1>
    </>
  );
}

Component Composition Patterns

Props and Children

Basic component with props:

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  onClick?: () => void;
  children: React.ReactNode;
}

function Button({ variant = 'primary', size = 'md', onClick, children }: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

Composition with children:

interface CardProps {
  children: React.ReactNode;
  className?: string;
}

function Card({ children, className = '' }: CardProps) {
  return (
    <div className={`card ${className}`}>
      {children}
    </div>
  );
}

// Usage
function UserProfile() {
  return (
    <Card>
      <h2>John Doe</h2>
      <p>Software Engineer</p>
    </Card>
  );
}

Lifting State Up

Shared state between siblings:

function Parent() {
  const [activeIndex, setActiveIndex] = useState(0);
  
  return (
    <>
      <Panel
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Panel 1 content
      </Panel>
      <Panel
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        Panel 2 content
      </Panel>
    </>
  );
}

interface PanelProps {
  isActive: boolean;
  onShow: () => void;
  children: React.ReactNode;
}

function Panel({ isActive, onShow, children }: PanelProps) {
  return (
    <div>
      <button onClick={onShow}>Show</button>
      {isActive && <div>{children}</div>}
    </div>
  );
}

Performance Optimization

Avoid Unnecessary Effects

❌ Bad - Using effect for derived state:

function TodoList({ todos }: { todos: Todo[] }) {
  const [visibleTodos, setVisibleTodos] = useState<Todo[]>([]);
  
  useEffect(() => {
    setVisibleTodos(todos.filter(t => !t.completed));
  }, [todos]); // Unnecessary effect
  
  return <ul>{/* ... */}</ul>;
}

✅ Good - Compute during render:

function TodoList({ todos }: { todos: Todo[] }) {
  const visibleTodos = todos.filter(t => !t.completed); // Direct computation
  
  return <ul>{/* ... */}</ul>;
}

useMemo for Expensive Computations

import { useMemo } from 'react';

function DataTable({ data }: { data: Item[] }) {
  const sortedData = useMemo(() => {
    return [...data].sort((a, b) => a.name.localeCompare(b.name));
  }, [data]); // Only recompute when data changes
  
  return <table>{/* render sortedData */}</table>;
}

useCallback for Function Stability

import { useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() => {
    console.log('Clicked', count);
  }, [count]); // Recreate only when count changes
  
  return <ExpensiveChild onClick={handleClick} />;
}

TypeScript Best Practices

Type-Safe Props

interface UserProps {
  id: string;
  name: string;
  email: string;
  age?: number; // Optional
}

function User({ id, name, email, age }: UserProps) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
      {age && <p>Age: {age}</p>}
    </div>
  );
}

Generic Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage
<List 
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
/>

Event Handlers

function Form() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Handle form submission
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
    </form>
  );
}

Common Patterns

Controlled Components

function ControlledInput() {
  const [value, setValue] = useState('');
  
  return (
    <input 
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

Conditional Rendering

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
  return (
    <div>
      {isLoggedIn ? (
        <UserGreeting />
      ) : (
        <GuestGreeting />
      )}
    </div>
  );
}

Lists and Keys

function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

Best Practices

General React Best Practices

  1. Dependency Arrays: Always specify correct dependencies in useEffect
  2. State Structure: Keep state minimal and avoid redundant state
  3. Component Size: Keep components small and focused
  4. Custom Hooks: Extract complex logic into reusable custom hooks
  5. TypeScript: Use TypeScript for type safety
  6. Keys: Use stable IDs as keys for list items, not array indices
  7. Immutability: Never mutate state directly
  8. Effects: Use effects only for synchronization with external systems
  9. Performance: Profile before optimizing with useMemo/useCallback

React 19 Specific Best Practices

  1. Server Components: Use Server Components for data fetching and static content
  2. Client Components: Mark components as 'use client' only when necessary
  3. Actions: Use Server Actions for mutations and form submissions
  4. Optimistic Updates: Implement useOptimistic for better UX
  5. use() Hook: Use for reading promises and context conditionally
  6. Form State: Use useFormState and useFormStatus for complex forms
  7. Concurrent Features: Leverage useTransition for non-urgent updates
  8. Error Boundaries: Implement proper error handling with error boundaries

Common Pitfalls

General React Pitfalls

Missing Dependencies:

useEffect(() => {
  // Uses 'count' but doesn't include it in deps
  console.log(count);
}, []); // Wrong!

Mutating State:

const [items, setItems] = useState([]);
items.push(newItem); // Wrong! Mutates state
setItems(items); // Won't trigger re-render

Correct Approach:

setItems([...items, newItem]); // Create new array

React 19 Specific Pitfalls

Using use() outside of render:

// Wrong!
function handleClick() {
  const data = use(promise); // Error: use() can only be called in render
}

Correct usage:

function Component({ promise }) {
  const data = use(promise); // Correct: called during render
  return <div>{data}</div>;
}

Forgetting 'use server' directive:

// Wrong - missing 'use server'
export async function myAction() {
  // This will run on the client!
}

Correct Server Action:

'use server'; // Must be at the top

export async function myAction() {
  // Now runs on the server
}

Mixing Server and Client logic incorrectly:

// Wrong - trying to use browser APIs in Server Component
export default async function ServerComponent() {
  const width = window.innerWidth; // Error: window is not defined
  return <div>{width}</div>;
}

Correct separation:

// Server Component for data
export default async function ServerComponent() {
  const data = await fetchData();
  return <ClientComponent data={data} />;
}

// Client Component for browser APIs
'use client';

function ClientComponent({ data }) {
  const [width, setWidth] = useState(window.innerWidth);
  // Handle resize logic...
  return <div>{width}</div>;
}

React 19 New Features

use() Hook - Reading Resources

The use() hook reads the value from a resource like a Promise or Context:

import { use } from 'react';

// Reading a Promise in a component
function MessageComponent({ messagePromise }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

// Reading Context conditionally
function Button() {
  if (condition) {
    const theme = use(ThemeContext);
    return <button className={theme}>Click</button>;
  }
  return <button>Click</button>;
}

useOptimistic Hook - Optimistic UI Updates

Manage optimistic UI updates for async operations:

import { useOptimistic } from 'react';

function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, newTodo]
  );

  const handleSubmit = async (formData) => {
    const newTodo = { id: Date.now(), text: formData.get('text') };

    // Optimistically add to UI
    addOptimisticTodo(newTodo);

    // Actually add to backend
    await addTodo(newTodo);
  };

  return (
    <form action={handleSubmit}>
      {optimisticTodos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
      <input type="text" name="text" />
      <button type="submit">Add Todo</button>
    </form>
  );
}

useFormStatus Hook - Form State

Access form submission status from child components:

import { useFormStatus } from 'react';

function SubmitButton() {
  const { pending, data } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function ContactForm() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" />
      <SubmitButton />
    </form>
  );
}

useFormState Hook - Form State Management

Manage form state with error handling:

import { useFormState } from 'react';

async function submitAction(prevState: string | null, formData: FormData) {
  const email = formData.get('email') as string;

  if (!email.includes('@')) {
    return 'Invalid email address';
  }

  await submitToDatabase(email);
  return null;
}

function EmailForm() {
  const [state, formAction] = useFormState(submitAction, null);

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button type="submit">Subscribe</button>
      {state && <p className="error">{state}</p>}
    </form>
  );
}

Server Actions

Define server-side functions for form handling:

// app/actions.ts
'use server';

import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Validate input
  if (!title || !content) {
    return { error: 'Title and content are required' };
  }

  // Save to database
  const post = await db.post.create({
    data: { title, content }
  });

  // Update cache and redirect
  revalidatePath('/posts');
  redirect(`/posts/${post.id}`);
}

Server Components

Components that run exclusively on the server:

// app/posts/page.tsx - Server Component
async function PostsPage() {
  // Server-side data fetching
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  });

  return (
    <div>
      <h1>Latest Posts</h1>
      <PostsList posts={posts} />
    </div>
  );
}

// Client Component for interactivity
'use client';

function PostsList({ posts }: { posts: Post[] }) {
  const [selectedId, setSelectedId] = useState<number | null>(null);

  return (
    <ul>
      {posts.map(post => (
        <li
          key={post.id}
          onClick={() => setSelectedId(post.id)}
          className={selectedId === post.id ? 'selected' : ''}
        >
          {post.title}
        </li>
      ))}
    </ul>
  );
}

React Compiler

Automatic Optimization

React Compiler automatically optimizes your components:

// Before React Compiler - manual memoization needed
const ExpensiveComponent = memo(function ExpensiveComponent({
  data,
  onUpdate
}) {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      computed: expensiveCalculation(item)
    }));
  }, [data]);

  const handleClick = useCallback((id) => {
    onUpdate(id);
  }, [onUpdate]);

  return (
    <div>
      {processedData.map(item => (
        <Item
          key={item.id}
          item={item}
          onClick={handleClick}
        />
      ))}
    </div>
  );
});

// After React Compiler - no manual optimization needed
function ExpensiveComponent({ data, onUpdate }) {
  const processedData = data.map(item => ({
    ...item,
    computed: expensiveCalculation(item)
  }));

  const handleClick = (id) => {
    onUpdate(id);
  };

  return (
    <div>
      {processedData.map(item => (
        <Item
          key={item.id}
          item={item}
          onClick={handleClick}
        />
      ))}
    </div>
  );
}

Installation and Setup

# Install React Compiler
npm install -D babel-plugin-react-compiler@latest

# Install ESLint plugin for validation
npm install -D eslint-plugin-react-hooks@latest
// babel.config.js
module.exports = {
  plugins: [
    'babel-plugin-react-compiler', // Must run first!
    // ... other plugins
  ],
};
// vite.config.js for Vite users
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['babel-plugin-react-compiler'],
      },
    }),
  ],
});

Compiler Configuration

// babel.config.js with compiler options
module.exports = {
  plugins: [
    [
      'babel-plugin-react-compiler',
      {
        // Enable compilation for specific files
        target: '18', // or '19'
        // Debug mode for development
        debug: process.env.NODE_ENV === 'development'
      }
    ],
  ],
};

// Incremental adoption with overrides
module.exports = {
  plugins: [],
  overrides: [
    {
      test: './src/components/**/*.{js,jsx,ts,tsx}',
      plugins: ['babel-plugin-react-compiler']
    }
  ]
};

Advanced Server Components Patterns

Mixed Server/Client Architecture

// Server Component for data fetching
async function ProductPage({ id }: { id: string }) {
  const product = await fetchProduct(id);
  const related = await fetchRelatedProducts(id);

  return (
    <div>
      <ProductDetails product={product} />
      <ProductGallery images={product.images} />
      <RelatedProducts products={related} />
    </div>
  );
}

// Client Component for interactivity
'use client';

function ProductDetails({ product }: { product: Product }) {
  const [quantity, setQuantity] = useState(1);
  const [isAdded, setIsAdded] = useState(false);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>

      <QuantitySelector
        value={quantity}
        onChange={setQuantity}
      />

      <AddToCartButton
        productId={product.id}
        quantity={quantity}
        onAdded={() => setIsAdded(true)}
      />

      {isAdded && <p>Added to cart!</p>}
    </div>
  );
}

Server Actions with Validation

'use server';

import { z } from 'zod';

const checkoutSchema = z.object({
  items: z.array(z.object({
    productId: z.string(),
    quantity: z.number().min(1)
  })),
  shippingAddress: z.object({
    street: z.string().min(1),
    city: z.string().min(1),
    zipCode: z.string().regex(/^\d{5}$/)
  }),
  paymentMethod: z.enum(['credit', 'paypal', 'apple'])
});

export async function processCheckout(
  prevState: any,
  formData: FormData
) {
  // Extract and validate data
  const rawData = {
    items: JSON.parse(formData.get('items') as string),
    shippingAddress: {
      street: formData.get('street'),
      city: formData.get('city'),
      zipCode: formData.get('zipCode')
    },
    paymentMethod: formData.get('paymentMethod')
  };

  const result = checkoutSchema.safeParse(rawData);

  if (!result.success) {
    return {
      error: 'Validation failed',
      fieldErrors: result.error.flatten().fieldErrors
    };
  }

  try {
    // Process payment
    const order = await createOrder(result.data);

    // Update inventory
    await updateInventory(result.data.items);

    // Send confirmation
    await sendConfirmationEmail(order);

    // Revalidate cache
    revalidatePath('/orders');

    return { success: true, orderId: order.id };
  } catch (error) {
    return { error: 'Payment failed' };
  }
}

Concurrent Features

useTransition for Non-Urgent Updates

import { useTransition, useState } from 'react';

function SearchableList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const [filteredItems, setFilteredItems] = useState(items);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Update input immediately
    setQuery(e.target.value);

    // Transition the filter operation
    startTransition(() => {
      setFilteredItems(
        items.filter(item =>
          item.name.toLowerCase().includes(e.target.value.toLowerCase())
        )
      );
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search items..."
      />

      {isPending && <div className="loading">Filtering...</div>}

      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

useDeferredValue for Expensive UI

import { useDeferredValue, useMemo } from 'react';

function DataGrid({ data }: { data: DataRow[] }) {
  const [searchTerm, setSearchTerm] = useState('');
  const deferredSearchTerm = useDeferredValue(searchTerm);

  const filteredData = useMemo(() => {
    return data.filter(row =>
      Object.values(row).some(value =>
        String(value).toLowerCase().includes(deferredSearchTerm.toLowerCase())
      )
    );
  }, [data, deferredSearchTerm]);

  return (
    <div>
      <input
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
        placeholder="Search..."
        className={searchTerm !== deferredSearchTerm ? 'stale' : ''}
      />

      <DataGridRows
        data={filteredData}
        isStale={searchTerm !== deferredSearchTerm}
      />
    </div>
  );
}

Testing React 19 Features

Testing Server Actions

import { render, screen, fireEvent } from '@testing-library/react';
import { jest } from '@jest/globals';
import ContactForm from './ContactForm';

// Mock server action
const mockSubmitForm = jest.fn();

describe('ContactForm', () => {
  it('submits form with server action', async () => {
    render(<ContactForm />);

    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: 'test@example.com' }
    });

    fireEvent.click(screen.getByText('Submit'));

    expect(mockSubmitForm).toHaveBeenCalledWith(
      expect.any(FormData)
    );
  });

  it('shows loading state during submission', async () => {
    mockSubmitForm.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));

    render(<ContactForm />);

    fireEvent.click(screen.getByText('Submit'));

    expect(screen.getByText('Submitting...')).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('Submit')).toBeInTheDocument();
    });
  });
});

Testing Optimistic Updates

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { jest } from '@jest/globals';
import TodoList from './TodoList';

describe('useOptimistic', () => {
  it('shows optimistic update immediately', async () => {
    const mockAddTodo = jest.fn(() => new Promise(resolve => setTimeout(resolve, 100)));

    render(
      <TodoList
        todos={[]}
        addTodo={mockAddTodo}
      />
    );

    fireEvent.change(screen.getByPlaceholderText('Add a todo'), {
      target: { value: 'New todo' }
    });

    fireEvent.click(screen.getByText('Add'));

    // Optimistic update appears immediately
    expect(screen.getByText('New todo')).toBeInTheDocument();

    // Wait for actual submission
    await waitFor(() => {
      expect(mockAddTodo).toHaveBeenCalledWith({
        id: expect.any(Number),
        text: 'New todo'
      });
    });
  });
});

Performance Best Practices

React Compiler Guidelines

  1. Write Standard React Code: The compiler works best with idiomatic React patterns
  2. Avoid Manual Memoization: Let the compiler handle useMemo, useCallback, and memo
  3. Keep Components Pure: Avoid side effects in render
  4. Use Stable References: Pass stable objects as props
// Good: Clean, idiomatic React
function ProductCard({ product, onAddToCart }) {
  const [quantity, setQuantity] = useState(1);

  const handleAdd = () => {
    onAddToCart(product.id, quantity);
  };

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <input
        type="number"
        value={quantity}
        onChange={e => setQuantity(Number(e.target.value))}
        min="1"
      />
      <button onClick={handleAdd}>Add to Cart</button>
    </div>
  );
}

// Avoid: Manual optimization
function ProductCard({ product, onAddToCart }) {
  const [quantity, setQuantity] = useState(1);

  const handleAdd = useCallback(() => {
    onAddToCart(product.id, quantity);
  }, [product.id, quantity, onAddToCart]);

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <QuantityInput
        value={quantity}
        onChange={setQuantity}
      />
      <button onClick={handleAdd}>Add to Cart</button>
    </div>
  );
}

Server Components Best Practices

  1. Keep Server Components Server-Only: No event handlers, hooks, or browser APIs
  2. Minimize Client Components: Only use 'use client' when necessary
  3. Pass Data as Props: Serialize data when passing from Server to Client
  4. Use Server Actions for Mutations: Keep data operations on the server
// Good: Server Component for static content
async function ProductPage({ id }: { id: string }) {
  const product = await fetchProduct(id);

  return (
    <article>
      <header>
        <h1>{product.name}</h1>
        <p>{product.description}</p>
      </header>

      <img
        src={product.imageUrl}
        alt={product.name}
        width={600}
        height={400}
      />

      <PriceDisplay price={product.price} />
      <AddToCartForm productId={product.id} />
    </article>
  );
}

// Client Component only for interactivity
'use client';

function AddToCartForm({ productId }: { productId: string }) {
  const [isAdding, setIsAdding] = useState(false);

  async function handleSubmit() {
    setIsAdding(true);
    await addToCart(productId);
    setIsAdding(false);
  }

  return (
    <form action={handleSubmit}>
      <button type="submit" disabled={isAdding}>
        {isAdding ? 'Adding...' : 'Add to Cart'}
      </button>
    </form>
  );
}

Migration Guide

From React 18 to 19

  1. Update Dependencies:
npm install react@19 react-dom@19
  1. Adopt Server Components:

    • Identify data-fetching components
    • Remove client-side code from Server Components
    • Add 'use client' directive where needed
  2. Replace Manual Optimistic Updates:

// Before
function TodoList({ todos, addTodo }) {
  const [optimisticTodos, setOptimisticTodos] = useState(todos);

  const handleAdd = async (text) => {
    const newTodo = { id: Date.now(), text };
    setOptimisticTodos([...optimisticTodos, newTodo]);
    await addTodo(newTodo);
  };
}

// After
function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, newTodo]
  );

  const handleAdd = async (formData) => {
    const newTodo = { id: Date.now(), text: formData.get('text') };
    addOptimisticTodo(newTodo);
    await addTodo(newTodo);
  };
}
  1. Enable React Compiler:
    • Install babel-plugin-react-compiler
    • Remove manual memoization
    • Let the compiler optimize automatically

References

Weekly Installs
68
Installed on
claude-code60
antigravity47
opencode46
gemini-cli44
cursor43
codex30