React Router 中调用 Actions 的三种方式详解

211 阅读7分钟

本文详细介绍React Router 中 调用 action 的三种核心方式。 它们之间的主要区别在于如何触发以及调用后是否会引起页面导航

我们来逐一详细解析,并总结它们的适用场景。

核心概念:什么是“导航 (Navigation)”?

在 React Router 中,一个“导航”并不仅仅是 URL 的变化。它是一个完整的路由过渡流程,通常包括:

  1. URL 更新:浏览器的地址栏会更新。
  2. 历史记录:在浏览器的历史记录中添加一个新条目,用户可以点击“后退”按钮返回。
  3. 数据重新加载:触发新页面的 loader 函数,获取页面所需数据。
  4. 组件重新渲染:渲染目标路由的组件。

理解了这一点,我们再来看这三种方法就清晰多了。


方法一:使用 <Form> 组件(声明式)

这是最常见、最直接的调用 action 的方式。

import { Form } from "react-router";

function SomeComponent() {
  return (
    // A. 声明一个表单,指向特定 action
    <Form action="/projects/123" method="post">
      <input type="text" name="title" />
      <button type="submit">Submit</button>
    </Form>
  );
}

代码解析:

A. <Form action="/projects/123" method="post"> :

  • action="/projects/123" : 这个 action prop 明确告诉 React Router,当表单提交时,应该去调用 /projects/123 这个路由路径下定义的 action 函数。
  • method="post" : 这是关键。它告诉 React Router 这是一个数据提交操作,应该调用 action 而不是 loader
  • 行为: 当用户点击 "Submit" 按钮时,React Router 会阻止浏览器默认的全页面刷新,然后发起一次导航/projects/123

结果:会触发一次完整的导航

适用场景

  • 创建新资源后跳转:比如创建一个新的博客文章,成功后你希望用户直接跳转到新文章的详情页。
  • 用户登录/注册:成功后跳转到仪表盘或主页。
  • 任何你希望在数据提交后,页面发生完整跳转的场景。

方法二:使用 useSubmit Hook(命令式)

这种方式让你可以在任何 JavaScript 逻辑中手动触发一个 action,而不是必须通过用户点击表单的提交按钮。

import { useCallback } from "react";
import { useSubmit } from "react-router";

function useQuizTimer() {
  // A. 从 hook 中获取 submit 函数
  let submit = useSubmit();

  // B. 定义一个回调函数,在特定时机调用 submit
  let cb = useCallback(() => {
    // C. 手动提交数据和指定 action
    submit(
      { quizTimedOut: true }, // 要提交的数据
      { action: "/end-quiz", method: "post" } // 目标 action 和方法
    );
  }, [submit]);

  // D. 触发器:一个 10 分钟的计时器
  let tenMinutes = 10 * 60 * 1000;
  useFakeTimer(tenMinutes, cb);
}

代码解析:

A. let submit = useSubmit() : 获取一个可以手动触发提交的函数。
B. useCallback(...) : 使用 useCallback 包装回调函数是一个好习惯,可以避免不必要的重渲染。
C. submit(data, options) : 这是核心。

  • 第一个参数 { quizTimedOut: true } 是你要提交的数据,React Router 会自动处理它。
  • 第二个参数 { action: "/end-quiz", method: "post" } 明确指定了目标 action 的路径和方法。
    D. 触发器: 这里的例子是一个计时器,但它可以是任何事件,比如 WebSocket 消息、键盘快捷键(Ctrl+S 保存)等。

结果:和 <Form> 一样,会触发一次完整的导航。它只是触发方式不同。

适用场景

  • 自动保存:当用户停止输入一段时间后自动保存草稿。
  • 非标准 UI 交互:比如拖拽一个项目到“完成”区域时,触发更新状态的 action
  • 定时任务:如示例中的在线测验时间到了,自动提交试卷。

方法三:使用 useFetcher Hook(无导航交互)

这是最强大的方式,它允许你在不引起页面导航的情况下,与 actionloader 进行交互。

声明式

import { useFetcher } from "react-router";

function Task() {
  // A. 获取一个 fetcher 实例
  let fetcher = useFetcher();

  // B. 通过 fetcher 的状态来更新 UI
  let busy = fetcher.state !== "idle";

  return (
    // C. 使用 fetcher 专属的 Form 组件
    <fetcher.Form method="post" action="/update-task/123">
      <input type="text" name="title" />
      <button type="submit" disabled={busy}>
        {busy ? "Saving..." : "Save"}
      </button>
    </fetcher.Form>
  );
}

代码解析:

A. let fetcher = useFetcher() : fetcher 是一个独立的对象,它有自己的状态(state)、数据(data)和 Form 组件。
B. fetcher.state: fetcher 有自己的生命周期状态 ("idle", "submitting", "loading")。你可以用它来创建精细的 UI 反馈,比如禁用按钮或显示加载指示器,而这不会影响全局导航状态
C. <fetcher.Form> : 这个表单的行为和普通 <Form> 不同。当它提交时,它只会把请求发送给 action,然后更新 fetcher 自己的状态和数据,但不会触发导航
D. fetcher.submit(...) : 和 useSubmit 类似,提供了命令式的调用方式,同样不会触发导航

结果不会触发导航。URL 不会变,浏览器历史记录不会增加,当前页面的 loader 不会重新运行。

适用场景(非常广泛):

  • “点赞”或“收藏” :在一个文章列表页,你希望用户可以点赞,但不想因此刷新整个页面。
  • 添加购物车:在商品详情页点击“加入购物车”,只更新购物车图标上的数字,页面其他部分保持不变。
  • 更新列表中的单项:在一个 To-Do List 中,勾选完成一项任务。
  • 任何你希望在“后台”完成数据交互,只在局部更新 UI 的场景。

命令式

fetcher.submit 的命令式用法非常强大,它能让你在不依赖传统表单提交按钮的情况下,从任何 JavaScript 逻辑中触发后台数据交互。

最经典的例子就是自动保存 (Auto-Save) 功能。

想象一下,你在开发一个在线笔记应用。你希望用户在停止输入一小段时间后,系统能自动将笔记内容保存到服务器,而不是要求用户频繁地手动点击“保存”按钮。

下面我们就用 fetcher.submit 来实现这个功能。

场景:笔记编辑器的自动保存

目标:当用户在文本框中输入内容时,我们不希望每次按键都向服务器发送请求(这样会造成巨大的服务器压力)。我们希望在用户停止输入后约 1 秒钟,再将最新的笔记内容发送到服务器保存。


1. 组件代码 (NoteEditor.jsx)

这个组件会包含一个文本域用于编辑笔记,并使用 fetcher.submit 来实现自动保存逻辑。

import { useEffect, useState } from "react";
import { useFetcher } from "react-router-dom";

// 假设 NoteEditor 从 props 接收笔记的初始数据
export default function NoteEditor({ note }) {
  // 1. 使用 useFetcher 来获取一个 fetcher 实例
  const fetcher = useFetcher();

  // 2. 使用 React 的 state 来管理文本域的当前内容
  const [content, setContent] = useState(note.content);

  // 3. 这是实现自动保存的核心逻辑
  useEffect(() => {
    // 只有当用户输入的内容与笔记原始内容不同时,才准备提交
    if (content !== note.content) {
      // 设置一个计时器,延迟 1 秒后执行提交
      const timer = setTimeout(() => {
        console.log("自动保存已触发!");
        // 4. 命令式提交!
        // 我们手动调用 fetcher.submit 来发送数据
        fetcher.submit(
          { content: content }, // 要提交的数据
          {
            method: "post",
            action: `/notes/${note.id}/update`, // 目标 action
          }
        );
      }, 1000); // 延迟1秒

      // 清理函数:如果用户在1秒内又输入了新内容,
      // 旧的计时器会被清除,然后重新开始计时。
      // 这就是“防抖”(debounce)技术。
      return () => clearTimeout(timer);
    }
  }, [content, note.id, note.content, fetcher]);

  // 5. 根据 fetcher 的状态提供 UI 反馈
  const isSaving = fetcher.state === "submitting";

  return (
    <div>
      <h3>编辑笔记: {note.title}</h3>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows={10}
        style={{ width: "100%" }}
      />
      <div>
        {/* 根据 fetcher.state 显示不同的状态信息 */}
        {isSaving ? (
          <p><i>正在保存...</i></p>
        ) : (
          <p><i>所有更改已自动保存。</i></p>
        )}
      </div>
    </div>
  );
}

2. 代码分步解析

  1. const fetcher = useFetcher() : 我们获取了一个 fetcher 实例。这个实例独立于页面的导航状态,有自己的 statedata

  2. const [content, setContent] = useState(...) : 我们用一个本地 state 来实时追踪用户在文本框中输入的内容。

  3. useEffect(...) : 这是魔法发生的地方。

    • 这个 useEffect Hook 会监听 content 变量的变化。每当用户输入一个字符,content 就会改变,这个 Hook 就会重新运行。
    • 防抖 (Debounce) :我们没有立即调用 fetcher.submit,而是创建了一个 setTimeout 计时器。如果在 1 秒内用户没有再次输入(即 content 没有再次变化),计时器就会触发,执行提交。如果用户在 1 秒内又输入了,useEffect 的清理函数 clearTimeout(timer) 会取消上一个还没来得及执行的提交,并设置一个新的计时器。这确保了我们只在用户“暂停输入”时才发送请求。
  4. fetcher.submit(...) : 这是命令式提交的核心。我们没有使用 <fetcher.Form>,而是在 setTimeout 的回调函数里,直接调用了 fetcher.submit 方法,将最新的 content 发送给了 /notes/${note.id}/update 这个 action

  5. const isSaving = fetcher.state === "submitting" : fetcher 有自己的状态。当 fetcher.submit 被调用后,它的状态会从 "idle" 变为 "submitting"。当服务器的 action 处理完毕并返回响应后,状态会再次变回 "idle"。我们可以利用这个状态来给用户提供即时反馈,比如显示“正在保存...”。

对比:命令式 fetcher.submit vs. 声明式 <fetcher.Form>

<fetcher.Form> (声明式)fetcher.submit (命令式)
触发方式由用户交互直接触发,通常是点击一个 <button type="submit">由程序代码在任何需要的时机触发。
控制粒度较低。提交时机由用户决定。非常高。开发者可以精确控制何时提交、提交什么数据。
典型用例点赞按钮、收藏按钮、在一个列表中更新单个项目的状态。自动保存设置项的即时保存(如开关一个选项)、响应非标准UI事件(如拖拽)。

通过这个自动保存的例子,您可以看到 fetcher.submit 将“数据提交”这个行为从用户的直接操作中解耦出来,变成了开发者可以自由调用的一个工具,从而能够构建出更流畅、更智能的用户体验。

总结与对比

特性<Form>useSubmituseFetcher
调用方式声明式 (组件)命令式 (Hook)声明式 (<fetcher.Form>) 和命令式 (fetcher.submit)
是否引起导航
更新浏览器历史
触发全局加载否 (只影响自身状态)
核心用途页面级的数据提交和跳转程序化、事件驱动的页面跳转无需跳转的后台数据交互、局部 UI 更新

通过这三种方式的组合,React Router 为开发者提供了从简单的页面跳转到复杂的局部更新的全方位数据交互解决方案。