Back to Blog
·5 min read

React 19: What's New and How to Use It

React 19 brings Actions, the new use() hook, improved ref handling, and better support for async operations. Here's a practical guide to everything new.

ReactJavaScriptFrontend

React 19 is here, and it's packed with features that fundamentally improve how we handle forms, async operations, and component composition. Let's dive into what's new and how you can start using these features today.

Actions: Simplified Form Handling

Actions are the biggest addition in React 19. They let you handle form submissions with automatic pending states, error handling, and optimistic updates.

function ContactForm() {
  async function submitContact(formData) {
    "use server";
 
    const name = formData.get("name");
    const email = formData.get("email");
 
    await saveToDatabase({ name, email });
  }
 
  return (
    <form action={submitContact}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Submit</button>
    </form>
  );
}

useActionState Hook

For more control over form state, use the new useActionState hook:

import { useActionState } from "react";
 
function LoginForm() {
  const [state, formAction, isPending] = useActionState(
    async (previousState, formData) => {
      const email = formData.get("email");
      const password = formData.get("password");
 
      try {
        await authenticate(email, password);
        return { success: true };
      } catch (error) {
        return { error: error.message };
      }
    },
    { error: null, success: false }
  );
 
  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
 
      {state.error && <p className="error">{state.error}</p>}
 
      <button type="submit" disabled={isPending}>
        {isPending ? "Signing in..." : "Sign In"}
      </button>
    </form>
  );
}

The use() Hook

The use() hook is a new primitive for reading resources during render - including promises and context.

Reading Promises

import { use, Suspense } from "react";
 
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}
 
function UserProfile({ userPromise }) {
  // use() will suspend until the promise resolves
  const user = use(userPromise);
 
  return (
    <div className="profile">
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}
 
function App({ userId }) {
  // Create the promise at the top level
  const userPromise = fetchUser(userId);
 
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Conditional Context

Unlike useContext, you can call use() conditionally:

function StatusMessage({ showDetails }) {
  if (showDetails) {
    const theme = use(ThemeContext);
    return <DetailedStatus theme={theme} />;
  }
 
  return <SimpleStatus />;
}

useOptimistic Hook

Show optimistic UI updates while async operations are in progress:

import { useOptimistic } from "react";
 
function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [...currentTodos, newTodo]
  );
 
  async function handleSubmit(formData) {
    const title = formData.get("title");
    const newTodo = { id: crypto.randomUUID(), title, pending: true };
 
    // Immediately show the new todo
    addOptimisticTodo(newTodo);
 
    // Actually save it
    await addTodo(title);
  }
 
  return (
    <>
      <form action={handleSubmit}>
        <input name="title" placeholder="New todo..." />
        <button type="submit">Add</button>
      </form>
 
      <ul>
        {optimisticTodos.map((todo) => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
            {todo.title}
          </li>
        ))}
      </ul>
    </>
  );
}

Improved Ref Handling

Ref as a Prop

Function components can now accept ref as a regular prop - no more forwardRef:

// Before (React 18)
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />);
 
// After (React 19)
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

Ref Cleanup Functions

Refs can now return cleanup functions, similar to useEffect:

function VideoPlayer({ src }) {
  return (
    <video
      ref={(video) => {
        if (video) {
          video.play();
        }
 
        // Cleanup function
        return () => {
          video?.pause();
        };
      }}
      src={src}
    />
  );
}

Document Metadata Components

Render metadata directly in your components:

function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title} | My Blog</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={`/blog/${post.slug}`} />
 
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

React will automatically hoist these to the document head. No more need for Helmet or similar libraries!

Stylesheet Precedence

Control stylesheet loading order with the precedence prop:

function Component() {
  return (
    <>
      <link rel="stylesheet" href="/base.css" precedence="default" />
      <link rel="stylesheet" href="/theme.css" precedence="high" />
    </>
  );
}

Async Script Support

Load scripts asynchronously within components:

function Analytics() {
  return <script async src="https://analytics.example.com/script.js" />;
}

React deduplicates scripts and handles loading automatically.

Migration Tips

Here's how to smoothly transition to React 19:

  1. Update gradually: Most React 18 code works unchanged
  2. Replace forwardRef: Convert to the new ref prop pattern
  3. Adopt Actions: Start with new forms, then migrate existing ones
  4. Test useOptimistic: Great for improving perceived performance
// Quick migration example for forms
// Before
function OldForm() {
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);
 
  async function handleSubmit(e) {
    e.preventDefault();
    setIsPending(true);
    try {
      await submitForm(new FormData(e.target));
    } catch (err) {
      setError(err.message);
    } finally {
      setIsPending(false);
    }
  }
 
  return <form onSubmit={handleSubmit}>...</form>;
}
 
// After
function NewForm() {
  const [state, action, isPending] = useActionState(submitForm, {});
 
  return <form action={action}>...</form>;
}

Wrapping Up

React 19 represents a significant step forward in developer experience. Actions simplify form handling, use() makes async data fetching more intuitive, and the improved ref handling removes unnecessary boilerplate.

The best part? These features are designed to work with existing code, so you can adopt them incrementally as you build new features.

What feature are you most excited about? I'm particularly enjoying how Actions have simplified my form components.

🚀

Let's Work Together

Got an exciting project? I'd love to hear about it. Let's chat and make something amazing together.

hello@danhobson.co.uk
or find me on