---
title: 数据写入
sidebar_position: 4
---

# 数据写入

在[数据获取](/guides/basic-features/data/data-fetch)章节中，介绍了 Modern.js 获取数据的方式，你可能会遇到两个问题：

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

在 Modern.js 中，可以通过 Data Action 解决和实现。

## 什么是 Data Action

Modern.js 推荐使用[约定式路由](/guides/basic-features/routes)做路由的管理，每个路由组件（`layout.ts`，`page.ts` 或 `$.tsx`）都可以有一个同名的 `.data` 文件。这些文件可以导出一个 `action` 函数，我们称为 Data Action，开发者可以在合适的时机调用它：

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

在 `routes/user/layout.data.ts` 文件中，可以导出一个 Data Action 函数：

```ts title="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);
}
```

在路由组件中，你可以通过 `useFetcher` 获取对应 `submit` 函数，执行并触发 Data Action：

```tsx title="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` 函数中，可以通过[入参](/guides/basic-features/data/data-write.html#入参)获取提交的数据，最终发送数据到服务端。

`action` 函数执行完后，Modern.js 将自动执行 `loader` 函数代码，并更新对应的数据和视图。

![action flow](https://lf3-static.bytednsdoc.com/obj/eden-cn/ulkl/ljhwZthlaukjlkulzlp/action-flow.png)

## `action` 函数

与 `loader` 函数一样，`action` 函数有两个入参，`params` 和 `request`。

### params

`params` 是当路由为[动态路由](/guides/basic-features/routes#动态路由)时动态路由片段，会作为参数传入 `action` 函数：

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

// 访问 /user/123 时，函数的参数为 `{ params: { id: '123' } }`
export const action = async ({ params }: ActionFunctionArgs) => {
  const { id } = params;
  const res = await fetch(`https://api/user/${id}`);
  return res.json();
};
```

### request

`request` 是一个 [Fetch Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 实例。通过 `request`，可以在 action 函数中获取到浏览器端提交的数据，如 `request.json()`，`request.formData()`，`request.json()` 等。具体使用哪个 API，请参考[数据类型](#数据类型)。

```tsx title="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](https://developer.mozilla.org/en-US/docs/Web/API/Response) 实例，
可以通过 [`useActionData`](https://reactrouter.com/en/main/hooks/use-action-data) 访问 response 中内容。

## useSubmit 和 useFetcher

### 区别

你可以通过 [`useSubmit`](https://reactrouter.com/en/main/hooks/use-submit) 或 [`useFetcher`](https://reactrouter.com/en/main/hooks/use-fetcher) 调用 Data Action。它们的区别是通过
`useSubmit` 调用，会触发浏览器的导航，通过 `useFetcher` 则不会触发浏览器的导航。

useSubmit:

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

useFetcher:

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

`submit` 函数有两个入参，第一个入参是传递到服务端的 `formData`。第二个入参是可选参数，其中
- `method` 相当于表单提交时的 `method`，大部分写入数据的场景下，`method` 可以传入 `post`。
- `action` 用来指定触发哪个路由组件的 Data Action，如果未传入 `action` 入参，默认会触发当前路由组件的 action，即 `user/page.tsx` 组件或子组件中执行 submit，会触发 `user/page.data.ts` 中定义的 action。

:::info

更多 API 的信息可参考相关文档：[`useSubmit`](https://reactrouter.com/en/main/hooks/use-submit)、[`useFetcher`](https://reactrouter.com/en/main/hooks/use-fetcher)。

:::


### 数据类型

`submit` 函数的第一个入参，可以接受不同类型的值。

如 `FormData`：

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

或 `URLSearchParams` 类型的值：

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

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

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

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

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

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

你也可以通过第二个参数，指定为编码方式：

```tsx
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
```

或提交纯文本：

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

## 为什么要提供 Data Action

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

![traditional state manage](https://lf3-static.bytednsdoc.com/obj/eden-cn/ulkl/ljhwZthlaukjlkulzlp/action-state-manage.png)

而在 Modern.js 中，我们希望通过 Loader 和 Action 帮助开发者自动的同步浏览器端和服务端的状态：

![state manage](https://lf3-static.bytednsdoc.com/obj/eden-cn/ulkl/ljhwZthlaukjlkulzlp/action-state-manage1.png)

如果项目中组件共享的数据主要来自于服务端的状态，则无需在项目引入浏览器端状态管理库。可以使用 Data Loader 请求数据，通过 [`useRouteLoaderData`](/guides/basic-features/data/data-fetch.md) 在子组件中共享数据，使用 Data Action 修改和同步服务端的状态。


## CSR 和 SSR

与 Data Loader 一样，SSR 项目中，Data Action 是在服务端执行的（框架会自动发请求触发 Data Action），而在 CSR 项目中，Data Action 是在浏览器端执行的。
