A quick overview of React 19 (my notes)

A quick overview of React 19 (my notes)

1. Actions: Making async operations cleaner

Transition notes

When you wrap an async operation in a transition (using useTransition), React will:

  • Immediately set isPending to true

  • Keep the current UI interactive while the async operation is happening

  • Apply the state updates only after the async operation completes

  • Handle pending states and errors automatically

Code sample with useActionState

// Old way
function ProfileForm() {
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);

  async function handleSubmit() {
    setIsPending(true);
    try {
      await updateProfile();
      setIsPending(false);
    } catch (e) {
      setError(e);
      setIsPending(false);
    }
  }
}

// React 19 way with useActionState
function ProfileForm() {
  const [error, submitAction, isPending] = useActionState(
    async (prev, formData) => {
      try {
        await updateProfile(formData);
        return null; // no error
      } catch (e) {
        return e; // return error
      }
    },
    // initial state
    null
  );

  return (
    <form action={submitAction}>
      {/* Form fields */}
      <button disabled={isPending}>Submit</button>
      {error && <p>{error}</p>}
    </form>
  );
}

Async function into its own function. Keep it clean. Just pass the function and then the initial state.

2. Optimistic Updates - Show changes instantly

So nice when error it automatically reverts to original state.

function TodoList() {
  // Current state: ["Buy milk"]
  const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos);

  async function addTodo(formData) {
    const newTodo = formData.get("todo");
    // Immediately show: ["Buy milk", "Buy eggs"]
    setOptimisticTodos([...todos, newTodo]);

    // If API fails, React automatically reverts to original state
    await addTodoToServer(newTodo);
  }

  return (
    <form action={addTodo}>
      <input name="todo" />
      <ul>
        {optimisticTodos.map((todo) => (
          <li>{todo}</li>
        ))}
      </ul>
    </form>
  );
}

3. The use Hook - Handle promises in render

function Comments({ commentsPromise }) {
  // This will suspend rendering until comments load
  const comments = use(commentsPromise);

  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}

// Use it with Suspense
<Suspense fallback={<Spinner />}>
  <Comments commentsPromise={fetchComments()} />
</Suspense>;

"suspending" means temporarily pausing the rendering of a component while it waits for some data or resource to load. When a component suspends, React will show the nearest <Suspense> boundary's fallback content until the data is ready.

4. Better ref handling

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

// React 19 way - ref is just a prop!
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// New: Cleanup function for refs
<div
  ref={(node) => {
    if (node) {
      // Setup
      const handler = () => console.log("clicked");
      node.addEventListener("click", handler);

      // Return cleanup
      return () => {
        node.removeEventListener("click", handler);
      };
    }
  }}
/>;

Nice that refs have explicit cleanup functions. They run when component unmounts.

5. Simpler Context usage

// Old way
const ThemeContext = createContext('light');

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

// React 19 way
<ThemeContext value="dark">
  <App />
</ThemeContext>

This is nice to see. Not that I had a big issue with it. But I always appreciate cleaner code.

6. Document Metadata in Components

This is nice especially if you wanna do meta tags dynamically based on the data.

function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.summary} />
      <link rel="canonical" href={post.url} />

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

Since I use React Router 7 as a framework, this is handled for me there when using it lol.