You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Remix.run:ActionFunction返回HTTP错误时弹出Toast消息的推荐方案

在ActionFunction返回错误时弹出Toast且不破坏UI的实现方案

方案1:Action返回带错误状态的成功响应(推荐)

直接让Action不抛出异常,而是返回包含错误信息的成功响应,然后在组件中通过useActionState或useFormState获取状态,控制Toast显示。这种方式完全不需要依赖CatchBoundary,UI不会被替换。

示例代码:

Action函数:

// app/actions.ts
export async function updateUserAction(formData: FormData) {
  try {
    // 模拟业务逻辑
    const name = formData.get("name") as string;
    if (!name) throw new Error("用户名不能为空");
    
    // 假设这里是更新用户的API调用
    // await fetch("/api/user", { method: "PUT", body: formData });
    
    return { success: true, message: "用户信息更新成功" };
  } catch (err) {
    // 返回错误状态而非抛出异常
    return { 
      success: false, 
      error: `更新失败:${(err as Error).message}` 
    };
  }
}

主组件:

// app/user-profile/page.tsx
"use client";

import { useActionState } from "react";
import { updateUserAction } from "../actions";

export default function UserProfile() {
  // 初始化状态:无成功/错误信息
  const [actionState, formAction] = useActionState(updateUserAction, {
    success: false,
    error: "",
    message: ""
  });

  return (
    <div className="p-6">
      <form action={formAction} className="space-y-4">
        <div>
          <label className="block mb-1">用户名</label>
          <input 
            type="text" 
            name="name" 
            className="border p-2 rounded w-full"
          />
        </div>
        <button 
          type="submit" 
          className="bg-blue-500 text-white px-4 py-2 rounded"
        >
          保存
        </button>
      </form>

      {/* 根据Action状态显示Toast */}
      {actionState.error && (
        <div className="fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded">
          {actionState.error}
        </div>
      )}
      {actionState.success && (
        <div className="fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded">
          {actionState.message}
        </div>
      )}
    </div>
  );
}

方案2:客户端包装Action捕获错误

如果你的Action必须抛出异常(比如需要利用Next.js的错误日志或其他机制),可以在客户端用一个包装函数捕获错误,再通过useActionState传递状态,同样不会触发CatchBoundary替换UI。

示例代码:

"use client";

import { useActionState } from "react";
import { updateUserAction } from "../actions";

// 客户端包装Action,捕获异常并返回错误状态
async function wrappedAction(prevState: any, formData: FormData) {
  try {
    return await updateUserAction(formData);
  } catch (err) {
    // 保留之前的状态,只更新错误信息
    return { 
      ...prevState, 
      success: false, 
      error: (err as Error).message 
    };
  }
}

export default function UserProfile() {
  const [actionState, formAction] = useActionState(wrappedAction, {
    success: false,
    error: ""
  });

  return (
    <div className="p-6">
      <form action={formAction} className="space-y-4">
        <div>
          <label className="block mb-1">用户名</label>
          <input 
            type="text" 
            name="name" 
            className="border p-2 rounded w-full"
          />
        </div>
        <button 
          type="submit" 
          className="bg-blue-500 text-white px-4 py-2 rounded"
        >
          保存
        </button>
      </form>

      {actionState.error && (
        <div className="fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded">
          {actionState.error}
        </div>
      )}
    </div>
  );
}

方案3:全局Toast上下文配合CatchBoundary

如果需要统一管理所有路由的错误提示,可以创建全局Toast上下文,在CatchBoundary中触发Toast并重置错误,避免UI被替换。

步骤1:创建Toast上下文

// app/components/ToastContext.tsx
"use client";

import { createContext, useContext, useState, useCallback, useEffect } from "react";

type Toast = {
  id: string;
  message: string;
  type: "success" | "error";
};

type ToastContextType = {
  showToast: (message: string, type: "success" | "error") => void;
  toasts: Toast[];
};

const ToastContext = createContext<ToastContextType | undefined>(undefined);

export function ToastProvider({ children }: { children: React.ReactNode }) {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const showToast = useCallback((message: string, type: "success" | "error") => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type }]);
  }, []);

  // 自动关闭Toast
  useEffect(() => {
    const timers = toasts.map(toast => 
      setTimeout(() => {
        setToasts(prev => prev.filter(t => t.id !== toast.id));
      }, 3000)
    );

    return () => timers.forEach(t => clearTimeout(t));
  }, [toasts]);

  return (
    <ToastContext.Provider value={{ showToast, toasts }}>
      {children}
      {/* 全局Toast容器 */}
      <div className="fixed top-4 right-4 space-y-2 z-50">
        {toasts.map(toast => (
          <div
            key={toast.id}
            className={`px-4 py-2 rounded shadow-lg ${
              toast.type === "error" 
                ? "bg-red-500 text-white" 
                : "bg-green-500 text-white"
            }`}
          >
            {toast.message}
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
}

export function useToast() {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error("useToast must be used within a ToastProvider");
  }
  return context;
}

步骤2:配置CatchBoundary

// app/user-profile/CatchBoundary.tsx
"use client";

import { useEffect } from "react";
import { CatchBoundaryProps } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useToast } from "../components/ToastContext";
import UserProfile from "./page";

export default function CatchBoundary({ error, reset }: CatchBoundaryProps) {
  const { showToast } = useToast();

  useEffect(() => {
    // 显示错误Toast
    showToast(error.message, "error");
    // 重置错误状态,让页面回到正常UI
    setTimeout(() => reset(), 0);
  }, [error, reset, showToast]);

  // 返回原页面组件,不替换UI
  return <UserProfile />;
}

步骤3:在根布局中引入ToastProvider

// app/layout.tsx
import { ToastProvider } from "./components/ToastContext";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        <ToastProvider>{children}</ToastProvider>
      </body>
    </html>
  );
}

总结

  • 优先选择方案1:实现简单,完全在主组件中控制状态,无需依赖CatchBoundary,UI无替换风险。
  • 如果Action必须抛错,用方案2:客户端包装捕获错误,灵活度高。
  • 全局统一错误提示需求,用方案3:一次配置,全路由生效,避免重复代码。

内容的提问来源于stack exchange,提问作者nakulthebuilder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 16:36:10