React 19 Actions 与 NestJS 控制器:通过原生协议终结全栈表单状态管理的复杂性

停止你的 Redux 事故:React 19 Actions 与 NestJS 的“裸奔”式表单统治

各位老铁,欢迎来到这场关于“如何不用手动维护状态”的讲座。

在过去的几年里,前端界就像一个发了疯的装修队。为了填一个简单的登录表单,我们要引入 Redux、Zustand、Context API,甚至还要为了处理表单验证而写一堆样板代码。我们就像是在给一个穿裤子的人穿了十层秋裤——看着臃肿,跑起来绊脚,而且关键时刻,这裤子居然还会走光。

直到 React 19 出现了。它就像是一个穿着紧身衣的健美教练,手里拿着一把名为 createServerAction 的瑞士军刀,告诉你:“够了,别他妈再自己造轮子了。”

今天,我们要聊聊怎么用 React 19 Actions 和 NestJS 控制器,通过最原始的 HTTP 协议,把全栈表单状态管理的复杂性变成一场优雅的华尔兹。

第一部分:别再写“回调地狱”了,那是上个世纪的事

让我们先来看看曾经的日子,也就是所谓的“旧时代”做法。那时候,当你想提交一个表单,你脑子里得浮现出下面这幅画面:

// ❌ 过去的你:精神崩溃的代码
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const handleSubmit = async (e) => {
  e.preventDefault();
  setLoading(true);
  setError(null);

  try {
    // 你得手动拼 URL,手动处理 loading,手动处理错误
    const response = await axios.post('/api/login', formData);
    if (response.data.success) {
      // 成功后的逻辑...
    } else {
      // 失败逻辑...
    }
  } catch (err) {
    // 异常捕获逻辑...
  } finally {
    setLoading(false);
  }
};

看这段代码,是不是感觉像是在便秘?每提交一次,你得手动管理 loading 的开始和结束,还得手动处理错误对象。如果你有 10 个表单,你就得复制粘贴 10 次这种“屎山代码”。

React 19 Actions 的出现,就是为了终结这种苦役。

Actions 是什么?简单说,它就是一个运行在服务器上的异步函数,但 React 给它加了一个神奇的外挂。当你调用这个函数时,React 会自动帮你处理 HTTP 请求的细节,包括 loading 状态、错误处理、数据缓存,甚至重定向。

第二部分:NestJS 控制器——那个严肃但靠谱的后卫

既然前端要把活儿揽过来,那后端呢?后端不能只负责收钱(响应),还得负责守门(验证)。

在这个全栈架构中,NestJS 扮演的是“严厉的守门员”角色。它不会接受任何不符合规则的球(数据)。我们要做的,就是把 React 的 Actions 和 NestJS 的 Controller 连接起来,通过标准的 HTTP 协议传输数据。

1. 定义契约

首先,我们需要一个 DTO (Data Transfer Object)。这就像是外卖订单上的标准格式,必须严格遵守。

// nestjs/auth.dto.ts
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';

export class LoginDto {
  @IsEmail({}, { message: '这邮箱写得像乱码吧?' })
  email: string;

  @IsNotEmpty()
  @MinLength(6, { message: '密码短了点,安全系数不够' })
  password: string;
}

2. 控制器逻辑

然后,在 NestJS 的 Controller 中,我们定义一个处理函数。注意,这里没有复杂的 middleware 逻辑,我们直接用 ValidationPipe,让 NestJS 帮我们检查数据。

// nestjs/auth.controller.ts
import { Controller, Post, Body, UseGuards, ValidationPipe } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('api/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  // ValidationPipe 会自动把 body 解析成 LoginDto 实例
  // 如果验证失败,会直接抛出异常,不需要你手动判断
  async login(@Body(ValidationPipe) loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }
}

这就是 NestJS 的核心魅力:声明式编程。你告诉它要做什么(POST 请求),它告诉你能不能做(ValidationPipe)。如果 NestJS 返回 400 错误,React Actions 会自动捕获这个异常并告诉你“密码太短了”。

第三部分:React 19 Actions——服务器端的表单逻辑

好了,现在我们有了后端,接下来就是前端的重头戏。React 19 引入了 createServerAction。注意,这不是一个普通的 async 函数,它是一个“Action”。

这个 Action 在 React 内部会生成一个对象,这个对象可以像 props 一样传给组件。而且,它自带了一个 state 管理器。

让我们来看看怎么写这个 Action。

// components/auth-form.tsx
'use server'; // 关键!这告诉 React "我是在服务器运行的"

import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

// 引入我们的 NestJS 控制器处理函数(这里用模拟数据演示,实际是 fetch)
// 我们不直接调用 NestJS 的实例,而是通过 HTTP 请求调用它
async function loginAction(formData: FormData) {
  const email = formData.get('email');
  const password = formData.get('password');

  const response = await fetch('http://localhost:3000/api/auth/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  });

  const data = await response.json();

  if (!response.ok) {
    // 如果服务器返回错误,我们抛出一个 Error
    // React 19 会自动识别这个错误并处理 UI
    throw new Error(data.message || '登录失败');
  }

  // 登录成功,设置 Cookie 并跳转
  cookies().set('session-token', data.token, { httpOnly: true });
  redirect('/dashboard');
}

// 这是一个组件
export function LoginForm() {
  return (
    <form action={loginAction}>
      <input name="email" type="email" placeholder="你的邮箱" required />
      <input name="password" type="password" placeholder="你的密码" required />
      <button type="submit">
        {/* React 19 会自动处理这里的 loading 状态,你不用写任何 JS 逻辑! */}
        登录
      </button>
    </form>
  );
}

看到了吗?这个组件的代码干净得就像刚洗完澡。action={loginAction} 一行代码,就把整个表单生命周期托管给了 React。你甚至不需要写 loading 状态,当你点击按钮时,按钮会自动变灰并显示“加载中”。

第四部分:深入理解 useFormStateuseFormStatus

上面的例子是最基础的。但在真实场景中,你可能需要在提交成功后更新页面上的其他状态,或者显示更精细的错误信息。这时候,就需要用到 React 19 的两个新 Hook:useFormStateuseFormStatus

1. useFormState:管理提交后的状态

假设登录成功后,我们需要在页面顶部显示一个欢迎语,或者从服务器获取用户信息。

'use client'; // 这个组件需要客户端交互来读取状态

import { useFormState, useFormStatus } from 'react-dom';

// 定义状态类型:'idle' | 'loading' | 'success' | 'error'
type FormState = {
  status: 'idle' | 'loading' | 'success' | 'error';
  message?: string;
  user?: any;
};

export function AdvancedLoginForm() {
  // 初始状态
  const initialState: FormState = { status: 'idle' };

  // 这里的第二个参数是当前的 state,第一个参数是 action 函数
  const [state, formAction] = useFormState(loginAction, initialState);

  return (
    <form action={formAction}>
      <input name="email" type="email" placeholder="邮箱" required />
      <input name="password" type="password" placeholder="密码" required />

      {/* 表单按钮 */}
      <FormButton />

      {/* 状态反馈区域 */}
      <StatusMessage status={state.status} message={state.message} />
    </form>
  );
}

// 子组件:处理按钮的 loading 状态
function FormButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? '正在为你登录...' : '点击登录'}
    </button>
  );
}

// 子组件:展示消息
function StatusMessage({ status, message }: any) {
  if (status === 'loading') return <p className="text-blue-500">正在连接服务器...</p>;
  if (status === 'success') return <p className="text-green-500">欢迎回来,大佬!</p>;
  if (status === 'error') return <p className="text-red-500">哎呀,{message},请重试。</p>;
  return null;
}

这里发生了一个神奇的化学反应:Action 函数被挂载到了 <form> 标签上。当表单提交时,React 会自动拦截这个事件,调用 loginAction,并把 FormData 传给它。函数执行完毕后,它返回的 state 会被自动传递给 useFormState,触发组件重新渲染。

这就是 React 19 的“状态机”模式。表单本身就是一个状态机:空闲 -> 提交中 -> 成功/失败 -> 空闲。

第五部分:数据验证的双重保险

在 React 19 中,验证不仅在前端做,还在后端做。但 React 19 给了前端验证一种“特权”。

你可以在提交前先做验证,如果前端验证不通过,React 会自动抛出错误,根本不会把请求发给 NestJS。这大大减少了服务器的负担。

客户端验证示例

async function loginAction(formData: FormData) {
  const email = formData.get('email');
  const password = formData.get('password');

  // 1. 前端验证:快速失败
  if (!email || !email.includes('@')) {
    // 这里的 throw Error 会被 React 捕获
    throw new Error('这看起来不像个邮箱,老铁。');
  }

  // 2. 后端验证:不可篡改
  const response = await fetch('http://localhost:3000/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });

  const data = await response.json();

  if (!response.ok) {
    // 如果服务器返回错误,覆盖前端的错误信息
    throw new Error(data.message);
  }

  redirect('/dashboard');
}

第六部分:处理复杂表单与文件上传

当然,现实世界的表单往往很复杂。比如一个“创建新用户”的表单,包含姓名、角色、头像上传。React 19 和 NestJS 配合得天衣无缝。

React 端:文件上传

React 19 默认支持 <input type="file" />

'use client';

import { useFormState } from 'react-dom';

export function CreateUserForm() {
  const [state, formAction] = useFormState(createUserAction, { status: 'idle' });

  return (
    <form action={formAction} encType="multipart/form-data">
      <input name="name" type="text" placeholder="姓名" required />
      <select name="role">
        <option value="user">普通用户</option>
        <option value="admin">管理员</option>
      </select>

      {/* 文件上传不需要额外处理,FormData 会自动处理二进制数据 */}
      <input name="avatar" type="file" accept="image/*" required />

      <button type="submit">创建用户</button>
    </form>
  );
}

NestJS 端:接收文件

NestJS 使用 Multer 来处理文件上传。

import { Controller, Post, Body, UseGuards, UseInterceptors, UploadedFile, ValidationPipe } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';

@Controller('api/users')
export class UsersController {
  @Post()
  @UseInterceptors(FileInterceptor('avatar', {
    storage: diskStorage({
      destination: './uploads',
      filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname),
    }),
  }))
  async createUser(
    @Body(ValidationPipe) createUserDto: CreateUserDto,
    @UploadedFile() file: Express.Multer.File
  ) {
    // NestJS 会自动将 'avatar' 字段映射为 file 对象
    console.log(file.originalname);
    return this.userService.create(createUserDto, file);
  }
}

注意看这个 name="avatar"。在 React 的 <input type="file" name="avatar" /> 中定义的 name,必须与 NestJS 中 @UploadedFile 装饰器后面括号里的参数 ('avatar') 完全一致。这种命名一致性是 HTTP 协议的基础,非常直观,不会出错。

第七部分:乐观 UI —— 告别加载转圈圈的等待焦虑

这可能是 React 19 Actions 最令人兴奋的功能了。现在的用户体验中,点击按钮 -> 显示 loading -> 等待 2 秒 -> 显示成功,这中间的 2 秒是非常枯燥的。

React 19 允许你做乐观更新:在服务器还没响应之前,直接更新 UI。

乐观更新的实现

'use client';
import { useState } from 'react';
import { useFormState } from 'react-dom';

// 假设这是一个模拟的 API 调用,带有 2 秒延迟
async function likePostAction(formData: FormData) {
  await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟网络延迟

  const postId = formData.get('postId');
  const action = formData.get('action'); // like 或 unlike

  // 真正的后端逻辑...
  return { success: true, postId };
}

export function LikeButton({ postId, initialLiked }: any) {
  const [liked, setLiked] = useState(initialLiked);
  const [state, formAction] = useFormState(likePostAction, { status: 'idle' });

  // 这是一个包装函数,用于实现乐观 UI
  const handleLike = () => {
    // 1. 先更新本地状态(乐观)
    setLiked(true);

    // 2. 提交表单
    // 注意:React 19 的 Action 会先执行 handleLike 里的逻辑吗?
    // 不,React 19 的机制是:你调用 action,它会等待服务器返回。
    // 但我们可以利用 useEffect 或者 useState 的副作用来模拟。
    // 这里演示一个简化的乐观 UI 逻辑:

    const formData = new FormData();
    formData.append('postId', postId);
    formData.append('action', 'like');

    formAction(formData).then(() => {
        // 如果服务器成功了,保持乐观状态
        // 如果服务器失败了,我们有一个 hook 可以撤销
    });
  };

  // 这是一个状态钩子,用于检测是否应该撤销乐观更新
  const { pending } = useFormStatus();

  return (
    <form action={handleLike}>
      <button type="submit" disabled={pending || liked}>
        {liked ? '❤️ 已点赞' : '🤍 点赞'}
      </button>
    </form>
  );
}

实际上,React 19 有更优雅的 useOptimistic Hook。让我们看看 useOptimistic 是怎么写的。

import { useOptimistic, useFormState } from 'react-dom';

function LikeButton({ postId, initialLiked }: any) {
  const [liked, setLiked] = useState(initialLiked);

  // 1. 定义乐观状态:如果当前是未点赞,乐观状态就是点赞
  const [optimisticLiked, addOptimisticState] = useOptimistic(
    liked,
    // 这是一个 reducer 函数,告诉你如何转换状态
    (state, newValue: boolean) => newValue
  );

  const [state, formAction] = useFormState(likePostAction, { status: 'idle' });

  const handleSubmit = async () => {
    // 2. 调用 addOptimisticState 立即更新 UI
    addOptimisticState(true);

    // 3. 提交表单
    const formData = new FormData();
    formData.append('postId', postId);
    formData.append('action', 'like');

    // formAction 会等待 Promise resolve
    await formAction(formData);

    // 如果成功,setLiked 会被 formAction 返回的数据更新(如果有的话)
    // 但在这个简单例子里,我们主要依靠 optimistic
  };

  return (
    <button onClick={handleSubmit} disabled={state.pending}>
      {optimisticLiked ? '❤️ 已点赞' : '🤍 点赞'}
    </button>
  );
}

哇,这代码简洁得令人发指。用户点击按钮 -> UI 瞬间变成红色爱心 -> 2秒后服务器确认 -> 无变化(因为本来就是对的)。

如果服务器出错了怎么办?useFormState 会把错误状态带回来,useOptimistic 会自动退回到真实状态。用户完全感觉不到刚才那一瞬间的“假象”,体验丝滑得像黄油。

第八部分:服务器组件与 Actions 的完美配合

这是 React 19 全栈开发的核心:React Server Components (RSC)

你可以把大部分组件写成 Server Component。Server Component 不需要 use client,它们天生就能直接调用 Actions,甚至直接从数据库读取数据,而不需要经过 HTTP 请求。

// app/dashboard/page.tsx
// 这是一个 Server Component,默认运行在服务器上
// 它可以直接调用 Actions,而不需要客户端拦截

import { redirect } from 'next/navigation';
import { loginAction } from './auth-actions'; // 假设这个文件在 server 目录下

export default function DashboardPage() {
  return (
    <div>
      <h1>欢迎来到 Dashboard</h1>
      <p>这里是纯服务端渲染的内容。</p>

      <form action={loginAction}>
        <h2>快速登录</h2>
        <input name="email" type="email" />
        <input name="password" type="password" />
        <button type="submit">登录</button>
      </form>
    </div>
  );
}

注意,在 Server Component 中,<form action={loginAction}> 是合法的。React 19 会自动把这个 Action 挂载到表单上。当用户提交表单时,React 会生成一个 HTTP POST 请求,转发给 NestJS 控制器。

这意味着,你不需要写任何客户端 JavaScript 来处理表单逻辑!整个流程(数据库查询 -> 验证 -> Action 处理 -> UI 渲染)都在服务端完成了。

第九部分:错误处理与边界

在旧世界中,错误处理就像是在玩扫雷,不知道哪里会炸。

在 React 19 中,我们可以使用 ErrorBoundary 或者更高级的 notFound() 函数。

假设 NestJS 抛出了一个未处理的异常(比如数据库断连),NestJS 会返回 500 错误。React Action 会捕获这个错误。我们可以把这个错误展示给用户。

'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { useEffect } from 'react';

export function LoginWithErrorHandling() {
  const [state, formAction] = useFormState(loginAction, { status: 'idle' });

  // 如果状态是 error,我们可以在这里做特殊处理
  useEffect(() => {
    if (state.status === 'error') {
      console.error('Server Error:', state.message);
      // 这里可以触发全局通知,比如 Toast
    }
  }, [state]);

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <FormButton />
      {state.status === 'error' && <p className="text-red-500">{state.message}</p>}
    </form>
  );
}

而且,如果 NestJS 的错误没有被正确捕获,React 还支持捕获“组件错误”,防止整个页面崩溃。

第十部分:架构总结与“实战”建议

现在,让我们把这堆东西理一理。你现在的技术栈应该长这样:

  1. 前端 (Next.js App Router):
    • 使用 createServerAction 定义业务逻辑。
    • 使用 useFormStateuseOptimistic 管理状态。
    • 使用 <form action={...}><input type="file">
    • Server Components 负责布局和展示,Client Components 负责交互(如果需要)。
  2. 后端 (NestJS):
    • 使用 Controller 接收请求。
    • 使用 ValidationPipe (DTO) 进行数据清洗和验证。
    • 使用 Multer 处理文件上传。
    • 使用 Guard 和 Interceptors 进行权限控制和日志记录。
  3. 通信:
    • 纯粹的 HTTP (POST/GET)。
    • JSON Body (文本)。
    • FormData (文件)。

专家建议:

  • 不要过度封装: React 19 Actions 已经封装得很好了,不要为了图新鲜去再包一层 API Service。直接在组件里写 Action。
  • 善用 Zod: 如果你想在 NestJS 和 React 之间共享验证逻辑,可以使用 Zod。NestJS 的 class-validator 和 Zod 的类型提示可以完美配合。
  • 乐观更新是必须的: 除非你的表单提交需要几秒钟,否则一定要用 useOptimistic。这是用户体验的质变。
  • 不要在 Actions 里操作 DOM: Actions 运行在服务器上(或者构建后的函数中),没有浏览器 DOM。它只负责数据转换和请求转发。

结语:回归本质

我曾经看过一个很形象的比喻:表单就是用户在和你对话。

在 React 19 之前,你像个聋哑人,你得自己把对话翻译成 HTTP 请求,还得自己听懂对方的回话,还得自己更新 UI 的表情。

现在,React 19 Actions 和 NestJS 配合,就像是把你变成了一个外交官。你只要开口说话(定义表单),NestJS 就是你的翻译官和保镖,帮你检查对方说的话是不是脏话(验证),帮你把话传出去(请求),回来后把结果翻译成你能听懂的语言(State),并自动调整你的表情(UI 更新)。

这不仅仅是技术栈的升级,这是一种思维方式的解放。我们终于可以扔掉那些繁琐的 State Management 库,把精力花在构建真正的业务逻辑上。

所以,去拥抱 React 19 Actions 吧,去拥抱 NestJS,去享受那种代码少、逻辑清晰、用户体验极佳的开发过程。这就是全栈开发的真谛。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注