数据写入

在 Data Loader 章节中,介绍了 Modern.js 获取数据的方式,你可能会遇到两个问题:

  1. 如何更新 Data Loader 中的数据?
  2. 如何将新的数据传递到服务器?

EdenX 对此的解决方案是 DataAction。

基本示例

Data Action 和 Data Loader 一样,也是基于约定式路由的,通过 Modern.js 的约定式(嵌套)路由,每个路由组件(layout.tspage.ts$.tsx)可以有一个同名的 data 文件,data 文件中可以导出一个命名为 action 的函数。

.
└── routes
    └── user
        ├── layout.tsx
        └── layout.data.ts

在文件中定义以下代码:

routes/user/layout.data.ts
import type { ActionFunction } from '@modern-js/runtime/router';

export const action: ActionFunction = ({ request }) => {
    const newUser = await request.json();
    const name = newUser.name;
    return updateUserProfile(name);
}
routes/user/layout.tsx
import {
  useFetcher,
  useLoaderData,
  useParams,
  Outlet
} from '@modern-js/runtime/router';

export default () => {
  const userInfo = useLoaderData();
  const { submit } = useFetcher();
  const editUser = () => {
    const newUser = {
      name: 'Modern.js'
    }
    return submit(newUser, {
      method: 'post',
      encType: 'application/json',
    })
  }
  return (
    <div>
      <button onClick={editUser}>edit user</button>
      <div className="user-profile">
        {userInfo}
      </div>
      <Outlet context={userInfo}></Outlet>
    </div>
  )
}

这里当执行 submit 后,会触发定义的 action 函数;在 action 函数中,可以通过 request (request.json,request.formData)获取提交的数据,获取数据后,再发送数据到服务端。

而 action 函数执行完,会执行 loader 函数代码,并更新对应的数据和视图。

action flow

为什么要提供 Data Action

Modern.js 中提供 Data Action 主要是为了使 UI 和服务器的状态能保持同步,通过这种方式可以减少状态管理的负担, 传统的状态管理方式,会在客户端和远程分别持有状态:

traditional state manage

而在 Modern.js 中,我们希望通过 Loader 和 Action 帮助开发者自动的同步客户端和服务端的状态:

state manage

如果项目中组件共享的数据主要服务端的状态,则无需在项目引入客户端状态管理库,使用 Data Loader 请求数据,通过 useRouteLoaderData 在子组件中共享数据, 通过 Data Action 修改和同步服务端的状态。

action 函数

loader 函数一样,action 函数有两个入参,paramsrequest

params

当路由文件通过 [] 时,会作为动态路由,动态路由片段会作为参数传入 action 函数:

// routes/user/[id]/page.data.ts
import { ActionFunctionArgs } from '@modern-js/runtime/router';

export const action = async ({ params }: ActionFunctionArgs) => {
  const { id } = params;
  const res = await fetch(`https://api/user/${id}`);
  return res.json();
};

当访问 /user/123 时,action 函数的参数为 { params: { id: '123' } }

request

request 是一个 Fetch Request 实例。

通过 request,可以在 action 函数中获取到客户端提交的数据,如 request.json()request.formData()request.json() 等, 具体应该使用哪个 API,请参考数据类型

// routes/user/[id]/page.data.ts
import { ActionFunctionArgs } from '@modern-js/runtime/router';

export const action = async ({ request }: ActionFunctionArgs) => {
  const newUser = await request.json();
  return updateUser(newUser);
};

返回值

action 函数的返回值可以是任何可序列化的内容,也可以是一个 Fetch Response 实例, 可以通过 useActionData 访问 response 中内容。

useSubmit 和 useFetcher

区别

你可以通过 useSubmituseFetcher 调用 action,它们的区别是通过 useSubmit 调用 action,会触发浏览器的导航,通过 useFetcher 则不会触发浏览器的导航。

useSubmit:

const submit = useSubmit();
submit(null, { method: "post", action: "/logout" });

useFetcher:

const { submit } = useFetcher();
submit(null, { method: "post", action: "/logout" });

submit 函数有两个入参,methodactionmethod 相当于表单提交时的 method,大部分写入数据的场景下,method 可以传入 postaction 用来指定触发哪个路由组件的 action,如果未传入 action 入参,默认会触发当前路由组件的 action,即 user/page.tsx 组件或子组件中执行 submit, 会触发 user/page.data.ts 中定义的 action。

INFO

这两个 API 更多的信息可参考相关文档:

数据类型

submit 函数的第一个入参,可以接受不同类型的值。 如 FormData

let formData = new FormData();
formData.append("cheese", "gouda");
submit(formData);
// In the action, you can get the data by request.json

URLSearchParams 类型的值:

let searchParams = new URLSearchParams();
searchParams.append("cheese", "gouda");
submit(searchParams);
// In the action, you can get the data by request.json

或任意 URLSearchParams 构造函数可接受的值

submit("cheese=gouda&toasted=yes");
submit([
  ["cheese", "gouda"],
  ["toasted", "yes"],
]);
// In the action, you can get the data by request.json

默认情况下,如果 submit 函数中的第一个入参是一个对象,对应的数据会被 encode 为 formData

submit(
  { key: "value" },
  {
    method: "post",
    encType: "application/x-www-form-urlencoded",
  }
);

// In the action, you can get the data by request.formData

也可以指定为 json 编码:

submit(
  { key: "value" },
  { method: "post", encType: "application/json" }
);

submit('{"key":"value"}', {
  method: "post",
  encType: "application/json",
});

// In the action, you can get the data by request.json

或提交纯文本:

submit("value", { method: "post", encType: "text/plain" });
// In the action, you can get the data by request.text

CSR 和 SSR

与 Data Loader 一样,SSR 项目中,Data Action 是在服务端执行的(框架会自动发请求触发 Data Action),而在 CSR 项目中,Data Action 是在客户端执行的。