DEV Community

Collins Oromoni
Collins Oromoni

Posted on

React 19: A Comprehensive Technical Guide with Code Examples

React 19 is far more than a routine version bump in its ecosystem. It stands out for its efficiency and simplicity for both beginners and experienced developers.
This update introduces powerful new capabilities that simplify pre-existing solutions and development patterns, with application performance in mind.

**

Actions and useTransition Enhancements

**
React 19 introduces Actions, a more advanced approach to managing asynchronous operations with built-in state management. It seamlessly integrates with the improved useTransition hook, making concurrent updates and transitions smoother than ever.

React 19 code:

import { useTransition } from 'react';

function UpdateNameForm() {
  const [isPending, startTransition] = useTransition();
  const [name, setName] = useState('');

  async function updateName(formData) {
    const newName = formData.get('name');
    // Action automatically handles pending state
    await fetch('/api/update-name', {
      method: 'POST',
      body: JSON.stringify({ name: newName })
    });
    setName(newName);
  }

  return (
    <form action={updateName}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Updating...' : 'Update Name'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Previous React Version (React 18):

import { useTransition, useState } from 'react';

function UpdateNameForm() {
  const [isPending, startTransition] = useTransition();
  const [name, setName] = useState('');

  async function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    const newName = formData.get('name');

    startTransition(async () => {
      await fetch('/api/update-name', {
        method: 'POST',
        body: JSON.stringify({ name: newName })
      });
      setName(newName);
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Updating...' : 'Update Name'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

**

The useActionState Hook**
useActionState (formerly useFormState) offers a simpler and more unified method for handling form submissions alongside server actions. It easily combines state management with action handling, resulting in fewer lines of code and a clearer understanding.

React 19 Code:

import { useActionState } from 'react';

async function submitForm(previousState, formData) {
  const name = formData.get('name');
  const email = formData.get('email');

  try {
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify({ name, email })
    });
    return { success: true, message: 'Form submitted!' };
  } catch (error) {
    return { success: false, message: 'Submission failed' };
  }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitForm, {
    success: false,
    message: ''
  });

  return (
    <form action={formAction}>
      <input type="text" name="name" required />
      <input type="email" name="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Previous React Version (React 18):

import { useState } from 'react';

function ContactForm() {
  const [isPending, setIsPending] = useState(false);
  const [state, setState] = useState({
    success: false,
    message: ''
  });

  async function handleSubmit(e) {
    e.preventDefault();
    setIsPending(true);

    const formData = new FormData(e.target);
    const name = formData.get('name');
    const email = formData.get('email');

    try {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify({ name, email })
      });
      setState({ success: true, message: 'Form submitted!' });
    } catch (error) {
      setState({ success: false, message: 'Submission failed' });
    } finally {
      setIsPending(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" required />
      <input type="email" name="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

**

The useOptimistic Hook

**
The useOptimistic hook allows applications to update state instantly with the expected result, even before the real operation finishes in the background. This makes applications seem faster and smoother, creating a responsive and smooth user experience.
React 19 Code:

import { useOptimistic, useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { id: Date.now(), text: newTodo, pending: true }]
  );

  async function addTodo(formData) {
    const text = formData.get('todo');
    addOptimisticTodo(text);

    const newTodo = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text })
    }).then(res => res.json());

    setTodos(current => [...current, newTodo]);
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
            {todo.text}
          </li>
        ))}
      </ul>
      <form action={addTodo}>
        <input type="text" name="todo" />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Previous React Version (React 18):

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [pendingTodos, setPendingTodos] = useState([]);

  async function addTodo(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    const text = formData.get('todo');

    // Manually add optimistic todo
    const tempId = Date.now();
    setPendingTodos(current => [...current, { id: tempId, text, pending: true }]);

    try {
      const newTodo = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text })
      }).then(res => res.json());

      setTodos(current => [...current, newTodo]);
    } finally {
      setPendingTodos(current => current.filter(t => t.id !== tempId));
    }
  }

  const allTodos = [...todos, ...pendingTodos];

  return (
    <div>
      <ul>
        {allTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
            {todo.text}
          </li>
        ))}
      </ul>
      <form onSubmit={addTodo}>
        <input type="text" name="todo" />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

**

The use() Hook

**
The use() hook is a revolutionary addition—it allows developers read Promises and context directly inside components, even conditionally. It opens up new, seamless ways to handle asynchronous data and shared context values.
React 19 Code (Reading Promises):

import { use, Suspense } from 'react';

async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

function UserProfile({ userPromise }) {
  // use() unwraps the Promise directly
  const user = use(userPromise);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  const userPromise = fetchUser(123);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Previous React Version (React 18):

import { useState, useEffect, Suspense } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
      setLoading(false);
    }
    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return <UserProfile userId={123} />;
}
Enter fullscreen mode Exit fullscreen mode

React 19 Code (Reading Context):
You can also use use() to read context—and unlike useContext, it can be invoked conditionally, providing more flexibility in component logic.

React 19

import { use, createContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton({ isSpecial }) {
  // use() can be called conditionally!
  const theme = isSpecial ? use(ThemeContext) : 'default';

  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      Click me
    </button>
  );
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton isSpecial={true} />
      <ThemedButton isSpecial={false} />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Previous React Version (React 18):

import { useContext, createContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton({ isSpecial }) {
  // useContext cannot be called conditionally
  const theme = useContext(ThemeContext);
  const finalTheme = isSpecial ? theme : 'default';

  return (
    <button style={{ background: finalTheme === 'dark' ? '#333' : '#fff' }}>
      Click me
    </button>
  );
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton isSpecial={true} />
      <ThemedButton isSpecial={false} />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Document Metadata Support
React 19 now supports rendering document metadata such as

, , and tags directly within components. This not only simplifies SEO and document configuration within React-based applications but also demonstrates the power of React 19.
React 19 Code:
function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title} - My Blog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <meta property="og:image" content={post.coverImage} />
      <link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />

      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Previous React Version (React 18):

import { useEffect } from 'react';
// Or using libraries like react-helmet

function BlogPost({ post }) {
  useEffect(() => {
    document.title = `${post.title} - My Blog`;

    // Update meta tags manually
    let metaDescription = document.querySelector('meta[name="description"]');
    if (!metaDescription) {
      metaDescription = document.createElement('meta');
      metaDescription.name = 'description';
      document.head.appendChild(metaDescription);
    }
    metaDescription.content = post.excerpt;

    // Cleanup is complex and error-prone
    return () => {
      document.title = 'My Blog';
    };
  }, [post.title, post.excerpt]);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Ref as a Prop
In React 19, forwardRef is no longer required. In React 19, ref can be passed directly to components without using forwardRef, making component composition easier and more uniform.
React 19 Code:



// Option 1: Don't destructure ref at all
function CustomInput(props) {
  return <input {...props} />;
}

// Option 2: Use a different prop name if you need to rename it
function CustomInput(props) {
  return <input ref={props.ref} {...props} />;
}

function ParentComponent() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Type here..." />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

Previous React Version (React 18):

import { forwardRef, useRef } from 'react';

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

function ParentComponent() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Type here..." />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

Improved Error Handling
Developers now benefit from more detailed reports and improved error boundaries that provide better information during runtime failures.
React 19 Code:

import { Component } from 'react';

class ErrorBoundary extends Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // React 19 provides more detailed error information
    console.error('Error caught:', {
      error,
      errorInfo,
      componentStack: errorInfo.componentStack, // Enhanced stack trace
      digest: errorInfo.digest // Error identifier for deduplication
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error?.message}</pre>
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

Previous React Version (React 18):

import { Component } from 'react';

class ErrorBoundary extends Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Less detailed error information
    console.error('Error caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
        </div>
      );
    }

    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

Async Scripts and Stylesheets
React 19 improves support for loading asynchronous scripts and stylesheets, providing greater control over application performance.
React 19 Code:

function ProductPage() {
  return (
    <div>
      {/* React 19 handles async loading and deduplication automatically */}
      <link rel="stylesheet" href="/styles/product.css" precedence="default" />
      <script async src="https://analytics.example.com/script.js" />

      <h1>Product Details</h1>
      <div className="product-content">
        {/* Content here */}
      </div>
    </div>
  );
}

function CheckoutPage() {
  return (
    <div>
      {/* Same stylesheet - React 19 deduplicates automatically */}
      <link rel="stylesheet" href="/styles/product.css" precedence="default" />
      <script async src="https://payment-provider.com/sdk.js" />

      <h1>Checkout</h1>
      <div className="checkout-content">
        {/* Content here */}
      </div>
    </div>
  );
}

Previous React Version (React 18):

import { useEffect } from 'react';

function ProductPage() {
  useEffect(() => {
    // Manually load stylesheet
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = '/styles/product.css';
    document.head.appendChild(link);

    // Manually load script
    const script = document.createElement('script');
    script.src = 'https://analytics.example.com/script.js';
    script.async = true;
    document.body.appendChild(script);

    // Cleanup required
    return () => {
      document.head.removeChild(link);
      document.body.removeChild(script);
    };
  }, []);

  return (
    <div>
      <h1>Product Details</h1>
      <div className="product-content">
        {/* Content here */}
      </div>
    </div>
  );
}

Conclusion
React 19 introduces a collection of powerful new Hooks that simplify complex patterns like form handling, optimistic UI updates, and asynchronous data fetching. The removal of forwardRef, native document metadata support, and the versatile use() hook collectively represent a significant leap in developer ergonomics.
These advancements make React code more intuitive, less bulky, and more expressive while maintaining backward compatibility for most existing applications. When migrating to React 19, ensure you review the official migration guide for potential breaking changes—especially around ref handling and deprecated APIs.
In essence, the new features in React 19 make your applications more maintainable, performant, and easier to build with with fewer lines of code.

Top comments (0)