You are viewing a preview of this lesson. Sign in to start learning
Back to React

React 19 Features

Leverage cutting-edge React 19 server-side capabilities

React 19 Features

Master React 19's latest capabilities with free flashcards and spaced repetition practice. This lesson covers the new Actions API, improved Server Components, enhanced use hook, and automatic batching improvementsβ€”essential features for building modern, performant React applications.

Welcome to React 19! πŸ’»

React 19 represents a significant leap forward in the React ecosystem, introducing features that fundamentally change how we handle asynchronous operations, manage server-client interactions, and optimize rendering performance. Released in 2024, this version focuses on developer experience and performance optimization while maintaining backward compatibility with most existing React applications.

These new features aren't just incremental improvementsβ€”they represent paradigm shifts in how we think about state management, data fetching, and the separation between client and server code. Whether you're building a simple dashboard or a complex enterprise application, React 19's features will streamline your development workflow.

Core Concepts 🎯

1. Actions API πŸš€

The Actions API is React 19's most transformative feature. It provides a unified way to handle asynchronous operations, particularly form submissions and data mutations, without the boilerplate code traditionally required for loading states, error handling, and optimistic updates.

What are Actions?

Actions are functions that can be passed to form elements or called manually to perform asynchronous operations. React automatically tracks the pending state of these actions and provides hooks to access their status.

function updateName(formData) {
  return fetch('/api/user', {
    method: 'POST',
    body: formData
  });
}

function ProfileForm() {
  return (
    <form action={updateName}>
      <input name="username" />
      <button type="submit">Update</button>
    </form>
  );
}

Key Benefits:

  • Automatic pending states: No need to manually track isLoading
  • Error boundaries integration: Errors are caught and handled gracefully
  • Optimistic updates: Update UI before server confirmation
  • Progressive enhancement: Forms work even without JavaScript

The useActionState Hook:

This hook provides access to the current state of an action, including pending status and return values.

import { useActionState } from 'react';

function MyForm() {
  const [state, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const name = formData.get('name');
      return await updateUser(name);
    },
    null // initial state
  );

  return (
    <form action={submitAction}>
      <input name="name" disabled={isPending} />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

πŸ’‘ Tip: The useActionState hook replaces the older useFormState hook. If you're migrating from React 18, update your imports!

2. Enhanced use Hook 🎣

React 19 introduces a revolutionary use hook that can read the value of a Promise or Context. Unlike other hooks, use can be called conditionally and even inside loopsβ€”breaking the traditional "Rules of Hooks."

Reading Promises with use:

import { use } from 'react';

function UserProfile({ userPromise }) {
  const user = use(userPromise);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  const userPromise = fetchUser(123);
  
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Reading Context with use:

import { use, createContext } from 'react';

const ThemeContext = createContext('light');

function Button({ primary }) {
  // Can be called conditionally!
  const theme = primary ? use(ThemeContext) : 'default';
  
  return <button className={theme}>Click me</button>;
}

Why This Matters:

Traditional Approach With use Hook
Fetch in useEffect, store in state Pass Promise directly, let React handle it
Manual loading/error states Suspense boundaries handle loading automatically
Cannot conditionally read context Conditional context reading is allowed
Waterfall loading issues Parallel data fetching out of the box

πŸ€” Did you know? The use hook is the first React hook that can be called conditionally. This breaks a fundamental rule that existed since hooks were introduced in React 16.8!

3. Server Components Improvements πŸ–₯️

React 19 significantly enhances Server Components, making them more powerful and easier to use. Server Components render on the server and send only the resulting HTML and minimal JavaScript to the client.

New Server Component Features:

// app/products/page.jsx (Server Component)
import { db } from '@/lib/database';

export default async function ProductsPage() {
  // Direct database access - no API route needed!
  const products = await db.query('SELECT * FROM products');
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} {...product} />
      ))}
    </div>
  );
}

Server Actions:

Server Actions are async functions that run on the server but can be called from client components:

'use server'

export async function createProduct(formData) {
  const name = formData.get('name');
  const price = formData.get('price');
  
  await db.insert({ name, price });
  revalidatePath('/products');
}

Client component using the server action:

'use client'

import { createProduct } from './actions';
import { useActionState } from 'react';

export default function ProductForm() {
  const [state, formAction] = useActionState(createProduct, null);
  
  return (
    <form action={formAction}>
      <input name="name" required />
      <input name="price" type="number" required />
      <button type="submit">Create Product</button>
    </form>
  );
}

Benefits of Server Components:

  • Zero bundle size: Server Components don't add to your JavaScript bundle
  • Direct backend access: Query databases, file systems, or internal APIs directly
  • Automatic code splitting: Only client component code is shipped to browsers
  • SEO friendly: Fully rendered HTML sent to crawlers

🌍 Real-world analogy: Think of Server Components like a restaurant kitchen (server) vs. the dining area (client). The kitchen (server) does all the heavy preparation work, and only the finished dish (rendered HTML) comes to your table (browser).

4. Document Metadata Management πŸ“„

React 19 introduces native support for document metadata like titles and meta tags, eliminating the need for third-party libraries like react-helmet.

function BlogPost({ post }) {
  return (
    <>
      <title>{post.title} - My Blog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:image" content={post.image} />
      <link rel="canonical" href={post.url} />
      
      <article>
        <h1>{post.title}</h1>
        <p>{post.content}</p>
      </article>
    </>
  );
}

React automatically hoists these tags to the document <head>, regardless of where they appear in your component tree. This works seamlessly with Server Components and Suspense.

5. Asset Loading Optimization 🎨

React 19 provides new APIs for optimizing resource loading, helping you eliminate layout shifts and improve Core Web Vitals scores.

Preloading Resources:

import { preload, preinit } from 'react-dom';

function MyComponent() {
  // Preload a resource
  preload('/fonts/CustomFont.woff2', { as: 'font' });
  
  // Preinit a stylesheet (loads and applies immediately)
  preinit('/styles/critical.css', { as: 'style' });
  
  return <div>Content here</div>;
}

Suspense for Images:

React 19 improves Suspense integration with images:

function Gallery({ images }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {images.map(img => (
        <img 
          key={img.id} 
          src={img.url} 
          loading="lazy"
          fetchPriority="high"
        />
      ))}
    </Suspense>
  );
}

6. useOptimistic Hook 🎯

The useOptimistic hook allows you to show optimistic UI updates while an asynchronous action is in progress, then revert if the action fails.

import { useOptimistic } from 'react';

function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, pending: true }]
  );
  
  async function handleSubmit(formData) {
    const newTodo = { id: Date.now(), text: formData.get('text') };
    addOptimisticTodo(newTodo);
    await addTodo(newTodo);
  }
  
  return (
    <>
      <form action={handleSubmit}>
        <input name="text" />
        <button>Add</button>
      </form>
      
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
            {todo.text}
          </li>
        ))}
      </ul>
    </>
  );
}

πŸ’‘ Tip: Use useOptimistic for operations like likes, follows, or adding items to a cartβ€”anywhere instant feedback improves user experience.

7. ref as a Prop 🎯

In React 19, you can now pass ref as a regular prop instead of using forwardRef. This significantly simplifies component APIs.

Before (React 18):

import { forwardRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

After (React 19):

function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

This works automatically in function components. No forwardRef wrapper needed!

8. Improved Error Handling πŸ›‘οΈ

React 19 enhances error reporting with better error boundaries and hydration error messages.

function ErrorBoundary({ fallback, children }) {
  return (
    <ErrorBoundary 
      fallback={({ error, resetError }) => (
        <div>
          <h2>Something went wrong</h2>
          <p>{error.message}</p>
          <button onClick={resetError}>Try again</button>
        </div>
      )}
    >
      {children}
    </ErrorBoundary>
  );
}

Hydration mismatches now show detailed diffs, making debugging much easier:

Hydration error:
Expected server HTML:
  <div>Server: 2024-01-15</div>
Received client HTML:
  <div>Client: 2024-01-16</div>

Detailed Examples πŸ”¨

Example 1: Building a Form with Actions API

Let's build a complete contact form using React 19's Actions API:

'use client'

import { useActionState } from 'react';

async function submitContactForm(previousState, formData) {
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');
  
  // Validation
  if (!name || name.length < 2) {
    return { error: 'Name must be at least 2 characters' };
  }
  
  if (!email.includes('@')) {
    return { error: 'Please enter a valid email' };
  }
  
  try {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, email, message })
    });
    
    if (!response.ok) throw new Error('Failed to submit');
    
    return { success: 'Message sent successfully!' };
  } catch (error) {
    return { error: 'Failed to send message. Please try again.' };
  }
}

export default function ContactForm() {
  const [state, formAction, isPending] = useActionState(
    submitContactForm,
    null
  );
  
  return (
    <form action={formAction} className="contact-form">
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          id="name" 
          name="name" 
          required 
          disabled={isPending}
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          id="email" 
          name="email" 
          type="email" 
          required 
          disabled={isPending}
        />
      </div>
      
      <div>
        <label htmlFor="message">Message:</label>
        <textarea 
          id="message" 
          name="message" 
          required 
          disabled={isPending}
        />
      </div>
      
      <button type="submit" disabled={isPending}>
        {isPending ? 'Sending...' : 'Send Message'}
      </button>
      
      {state?.error && (
        <div className="error" role="alert">
          ❌ {state.error}
        </div>
      )}
      
      {state?.success && (
        <div className="success" role="status">
          βœ… {state.success}
        </div>
      )}
    </form>
  );
}

Key Features:

  • Automatic pending state management with isPending
  • Built-in error handling through return values
  • Form inputs automatically disabled during submission
  • No manual useState or useEffect needed!
  • Progressive enhancement: works without JavaScript

Example 2: Data Fetching with the use Hook

Here's how to build a user dashboard that fetches data in parallel:

import { use, Suspense } from 'react';

// Fetch functions return Promises
function fetchUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

function fetchPosts(userId) {
  return fetch(`/api/posts?user=${userId}`).then(r => r.json());
}

function fetchComments(userId) {
  return fetch(`/api/comments?user=${userId}`).then(r => r.json());
}

// Component reads from Promise using 'use'
function UserInfo({ userPromise }) {
  const user = use(userPromise);
  
  return (
    <div className="user-info">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}

function PostsList({ postsPromise }) {
  const posts = use(postsPromise);
  
  return (
    <div className="posts">
      <h3>Recent Posts</h3>
      {posts.map(post => (
        <article key={post.id}>
          <h4>{post.title}</h4>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

function CommentsList({ commentsPromise }) {
  const comments = use(commentsPromise);
  
  return (
    <div className="comments">
      <h3>Recent Comments</h3>
      {comments.map(comment => (
        <div key={comment.id}>
          <p>{comment.text}</p>
        </div>
      ))}
    </div>
  );
}

// Main dashboard component
export default function Dashboard({ userId }) {
  // Start all fetches immediately (parallel!)
  const userPromise = fetchUser(userId);
  const postsPromise = fetchPosts(userId);
  const commentsPromise = fetchComments(userId);
  
  return (
    <div className="dashboard">
      <Suspense fallback={<UserSkeleton />}>
        <UserInfo userPromise={userPromise} />
      </Suspense>
      
      <div className="content-grid">
        <Suspense fallback={<PostsSkeleton />}>
          <PostsList postsPromise={postsPromise} />
        </Suspense>
        
        <Suspense fallback={<CommentsSkeleton />}>
          <CommentsList commentsPromise={commentsPromise} />
        </Suspense>
      </div>
    </div>
  );
}

Why This Pattern is Powerful:

Traditional useEffect Pattern:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Component Mounts                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Fetch User (500ms)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Fetch Posts (300ms)   β”‚  ← Waterfall!
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Fetch Comments (200ms)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Total: 1000ms ⏱️

React 19 use Hook Pattern:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Component Mounts                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Fetch User (500ms)    β”‚ ←┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ Parallel!
β”‚ Fetch Posts (300ms)   β”‚ ←─
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚ Fetch Comments (200ms)β”‚ β†β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Total: 500ms ⏱️

Example 3: Optimistic UI Updates

Building a like button with instant feedback:

'use client'

import { useOptimistic } from 'react';

async function likePost(postId) {
  const response = await fetch(`/api/posts/${postId}/like`, {
    method: 'POST'
  });
  
  if (!response.ok) throw new Error('Failed to like post');
  return response.json();
}

function Post({ id, title, content, initialLikes, initialLiked }) {
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    { count: initialLikes, liked: initialLiked },
    (state, newLiked) => ({
      count: state.count + (newLiked ? 1 : -1),
      liked: newLiked
    })
  );
  
  async function handleLike() {
    try {
      const newLiked = !optimisticLikes.liked;
      setOptimisticLikes(newLiked);
      await likePost(id);
    } catch (error) {
      // React automatically reverts on error
      console.error('Failed to update like:', error);
    }
  }
  
  return (
    <article>
      <h2>{title}</h2>
      <p>{content}</p>
      
      <button 
        onClick={handleLike}
        className={optimisticLikes.liked ? 'liked' : ''}
      >
        {optimisticLikes.liked ? '❀️' : '🀍'} 
        {optimisticLikes.count}
      </button>
    </article>
  );
}

The UI updates instantly when you click, then reverts automatically if the server request fails. This creates a snappy, app-like experience.

Example 4: Server Actions with Server Components

Building a blog with server-side rendering and mutations:

// app/blog/actions.js
'use server'

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/database';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');
  const author = formData.get('author');
  
  if (!title || !content) {
    return { error: 'Title and content are required' };
  }
  
  try {
    await db.posts.insert({
      title,
      content,
      author,
      createdAt: new Date()
    });
    
    revalidatePath('/blog');
    return { success: true };
  } catch (error) {
    return { error: 'Failed to create post' };
  }
}

export async function deletePost(postId) {
  await db.posts.delete({ id: postId });
  revalidatePath('/blog');
}
// app/blog/page.jsx (Server Component)
import { db } from '@/lib/database';
import { createPost } from './actions';
import PostForm from './PostForm';

export default async function BlogPage() {
  // Direct database access on server!
  const posts = await db.posts.findMany({
    orderBy: { createdAt: 'desc' }
  });
  
  return (
    <div>
      <h1>Blog</h1>
      
      <PostForm createAction={createPost} />
      
      <div className="posts">
        {posts.map(post => (
          <article key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
            <small>By {post.author}</small>
          </article>
        ))}
      </div>
    </div>
  );
}
// app/blog/PostForm.jsx (Client Component)
'use client'

import { useActionState } from 'react';

export default function PostForm({ createAction }) {
  const [state, formAction, isPending] = useActionState(
    createAction,
    null
  );
  
  return (
    <form action={formAction}>
      <input 
        name="title" 
        placeholder="Post title"
        disabled={isPending}
      />
      
      <textarea 
        name="content" 
        placeholder="Post content"
        disabled={isPending}
      />
      
      <input 
        name="author" 
        placeholder="Your name"
        disabled={isPending}
      />
      
      <button type="submit" disabled={isPending}>
        {isPending ? 'Publishing...' : 'Publish Post'}
      </button>
      
      {state?.error && <div className="error">{state.error}</div>}
      {state?.success && <div className="success">Post published!</div>}
    </form>
  );
}

Architecture Overview:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CLIENT BROWSER                    β”‚
β”‚                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚  PostForm (Client Component)     β”‚         β”‚
β”‚  β”‚  β€’ Handles user interaction      β”‚         β”‚
β”‚  β”‚  β€’ Shows loading states          β”‚         β”‚
β”‚  β”‚  β€’ Calls server action           β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                β”‚                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚ formAction()
                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              SERVER                            β”‚
β”‚                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚  createPost (Server Action)      β”‚         β”‚
β”‚  β”‚  β€’ Validates input               β”‚         β”‚
β”‚  β”‚  β€’ Writes to database            β”‚         β”‚
β”‚  β”‚  β€’ Triggers revalidation         β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                β”‚                               β”‚
β”‚                β–Ό                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚  BlogPage (Server Component)     β”‚         β”‚
β”‚  β”‚  β€’ Fetches latest posts          β”‚         β”‚
β”‚  β”‚  β€’ Renders HTML on server        β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Common Mistakes ⚠️

1. ❌ Using use() in Regular Hooks

// WRONG - use() is not a replacement for useEffect
function MyComponent() {
  const data = use(fetch('/api/data')); // Missing Suspense boundary!
  return <div>{data.name}</div>;
}
// CORRECT - Wrap with Suspense
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <MyComponent />
    </Suspense>
  );
}

2. ❌ Forgetting to Mark Server Functions

// WRONG - Missing 'use server' directive
export async function updateUser(data) {
  await db.users.update(data);
}
// CORRECT
'use server'

export async function updateUser(data) {
  await db.users.update(data);
}

3. ❌ Mixing Client and Server Code Improperly

// WRONG - Trying to use browser APIs in Server Component
export default async function Page() {
  const width = window.innerWidth; // Error! No window on server
  return <div>Width: {width}</div>;
}
// CORRECT - Mark as client component
'use client'

export default function Page() {
  const [width, setWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return <div>Width: {width}</div>;
}

4. ❌ Not Handling Action Errors Properly

// WRONG - Unhandled errors crash the app
function MyForm() {
  const [state, formAction] = useActionState(async (prev, formData) => {
    await fetch('/api/submit', { body: formData }); // Might throw!
    return { success: true };
  }, null);
  
  return <form action={formAction}>...</form>;
}
// CORRECT - Always catch and return errors
function MyForm() {
  const [state, formAction] = useActionState(async (prev, formData) => {
    try {
      const response = await fetch('/api/submit', { 
        method: 'POST',
        body: formData 
      });
      
      if (!response.ok) {
        throw new Error('Submission failed');
      }
      
      return { success: true };
    } catch (error) {
      return { error: error.message };
    }
  }, null);
  
  return (
    <form action={formAction}>
      {/* form fields */}
      {state?.error && <div className="error">{state.error}</div>}
    </form>
  );
}

5. ❌ Creating New Promises on Every Render

// WRONG - New promise on every render!
function UserProfile({ userId }) {
  const user = use(fetchUser(userId)); // fetchUser() called each render
  return <div>{user.name}</div>;
}
// CORRECT - Pass promise from parent
function App({ userId }) {
  const userPromise = useMemo(
    () => fetchUser(userId),
    [userId]
  );
  
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

function UserProfile({ userPromise }) {
  const user = use(userPromise);
  return <div>{user.name}</div>;
}

6. ❌ Misusing forwardRef When It's No Longer Needed

// WRONG in React 19 - forwardRef is unnecessary
import { forwardRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});
// CORRECT - ref is now a regular prop
function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

🧠 Memory Device for React 19 Features:

"AURA SO"

  • Actions API
  • Use hook
  • Ref as prop
  • Asset preloading
  • Server Components
  • Optimistic updates

Key Takeaways 🎯

βœ… Actions API simplifies async operations with automatic pending states and error handling

βœ… The use hook can read Promises and Context, even conditionallyβ€”breaking traditional hook rules

βœ… Server Components let you access backend resources directly without API routes

βœ… Server Actions enable mutations from client components while keeping business logic on the server

βœ… useOptimistic provides instant UI feedback with automatic rollback on errors

βœ… ref as a prop eliminates the need for forwardRef in most cases

βœ… Native metadata management removes dependency on third-party libraries

βœ… Improved asset preloading APIs optimize performance and Core Web Vitals

βœ… Enhanced error boundaries provide better debugging information

βœ… React 19 maintains backward compatibility with most React 18 code

πŸ“š Further Study

πŸ“‹ Quick Reference Card

Feature Hook/API Key Use Case
Actions useActionState() Form submissions with auto pending states
Promise Reading use(promise) Async data fetching without useEffect
Optimistic UI useOptimistic() Instant feedback for mutations
Server Actions 'use server' Backend mutations from client components
Asset Loading preload(), preinit() Optimize resource loading
Ref Passing ref as prop No forwardRef needed
Metadata <title>, <meta> Native document head management

πŸ”§ Try This: Migration Checklist

Moving from React 18 to React 19:

βœ“ Replace forwardRef with ref as prop
βœ“ Update useFormState to useActionState
βœ“ Remove react-helmet in favor of native metadata
βœ“ Consider Server Components for data-heavy pages
βœ“ Use Actions API for form submissions
βœ“ Add use() hook for cleaner async data fetching
βœ“ Implement useOptimistic() for better UX
βœ“ Test with Suspense boundaries around async components