Skip to content

React 20 Actions v2 Enhancements

The Actions mechanism in React 20 receives a major upgrade. v1 Actions mainly addressed form submission scenarios; v2 extends them into a general-purpose async operation primitive covering optimistic updates, error boundary integration, and transactional state management.

useActionState Redesigned

The v2 useActionState is no longer limited to form actions — it can now wrap any async function, with built-in pending state, optimistic updates, and automatic error recovery.

javascript
import { useActionState } from "react";

function CommentSection({ postId }) {
  const [comments, addComment, isPending] = useActionState(
    async (prevComments, formData) => {
      const text = formData.get("comment");
      const newComment = await api.addComment(postId, { text });
      return [...prevComments, newComment];
    },
    initialComments,
    {
      optimisticUpdate: (prevComments, formData) => {
        return [
          ...prevComments,
          {
            id: `temp-${Date.now()}`,
            text: formData.get("comment"),
            optimistic: true,
          },
        ];
      },
    },
  );

  return (
    <div>
      {comments.map((c) => (
        <Comment key={c.id} comment={c} isOptimistic={c.optimistic} />
      ))}
      <form action={addComment}>
        <textarea name="comment" required />
        <button disabled={isPending}>
          {isPending ? "发送中..." : "发表评论"}
        </button>
      </form>
    </div>
  );
}

The key change is the optimisticUpdate option — it makes optimistic updates declarative, eliminating the need to manually manage useOptimistic state rollbacks.

useAction: Actions for Non-Form Scenarios

useAction is a new hook in v2 for non-form-triggered async operations such as button clicks, drag-and-drop, and scheduled tasks.

javascript
import { useAction } from "react";

function FileUploader() {
  const { execute, isPending, error, data } = useAction(
    async (file) => {
      const presignedUrl = await api.getUploadUrl(file.name);
      await fetch(presignedUrl, {
        method: "PUT",
        body: file,
      });
      return api.confirmUpload(file.name);
    },
    {
      onSuccess: (result) => toast.success(`上传成功: ${result.name}`),
      onError: (err) => toast.error(err.message),
      retry: { count: 3, delay: 1000 },
    },
  );

  const handleDrop = (e) => {
    e.preventDefault();
    const file = e.dataTransfer.files[0];
    if (file) execute(file);
  };

  return (
    <div
      onDrop={handleDrop}
      onDragOver={(e) => e.preventDefault()}
      style={{ opacity: isPending ? 0.6 : 1 }}
    >
      {isPending ? "上传中..." : "拖拽文件到此处"}
      {error && <p className="error">{error.message}</p>}
    </div>
  );
}

The built-in retry mechanism is a standout feature — the retry config supports exponential backoff, which is particularly useful on mobile devices with weak network connectivity.

Deep Integration of Actions and Suspense

v2 Actions can directly drive Suspense boundaries, enabling a true "loading state as UI" pattern:

javascript
function OrderDashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <OrderStats />
      <Suspense fallback={<TableSkeleton />}>
        <OrderList />
      </Suspense>
    </Suspense>
  );
}

function OrderList() {
  const orders = use(api.getOrders()); // Read directly during render

  const [filtered, filterAction, isPending] = useActionState(
    async (prev, formData) => {
      const status = formData.get("status");
      return api.getOrders({ status });
    },
    orders,
  );

  return (
    <div style={{ transition: isPending ? "opacity 150ms" : undefined }}>
      <form action={filterAction}>
        <select name="status">
          <option value="all">全部</option>
          <option value="pending">待处理</option>
          <option value="shipped">已发货</option>
        </select>
      </form>
      <Table data={filtered} />
    </div>
  );
}

The transition combined with isPending gives filter operations a graceful fade effect without content flickering.

Summary

  • useActionState adds an optimisticUpdate option, making optimistic updates declarative
  • useAction hook covers non-form async scenarios with built-in retry and lifecycle callbacks
  • Actions deeply integrate with Suspense; pending state naturally drives UI transitions
  • Error recovery is smarter — automatically rolling back optimistic state on failure
  • Actions have become the standard paradigm for async operations in React; recommended as a replacement for the traditional useEffect + fetch pattern

MIT Licensed