React 19 useActionState表单状态管理与表单提交流程重构实战

React 19引入了useActionState Hook,将表单提交从手动管理loading/error状态的模式中解放出来。传统React表单处理需要维护多个state变量(isPending、error、data),在submit handler中手动控制异步流程。useActionState将这些逻辑收敛到一个Action函数中,配合form组件的action属性,表单提交的状态机由React框架自动管理。

useActionState基本用法

useActionState接收一个Action函数和初始状态,返回当前状态、包裹后的Action函数和isPending标志。Action函数签名为(previousState, formData) -> newState,在表单提交时被调用。

import { useActionState } from 'react';

function LoginForm() {
  async function loginAction(prevState, formData) {
    const email = formData.get('email');
    const password = formData.get('password');

    if (!email || !password) {
      return { error: '邮箱和密码不能为空' };
    }
    if (!email.includes('@')) {
      return { error: '邮箱格式不正确' };
    }

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

    if (!res.ok) {
      const data = await res.json();
      return { error: data.message || '登录失败' };
    }

    return { success: true, redirect: '/dashboard' };
  }

  const [state, formAction, isPending] = useActionState(loginAction, null);

  return (
    <form action={formAction}>
      <input type="email" name="email" required />
      <input type="password" name="password" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '登录中...' : '登录'}
      </button>
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  );
}

关键变化:不再需要useState管理error和isPending,不再需要onSubmit + preventDefault样板代码。表单提交的触发、Action函数调用、状态更新全部由React协调。

useOptimistic乐观更新集成

useOptimistic Hook提供乐观更新能力,在提交开始时立即更新UI,提交失败时自动回滚:

import { useActionState, useOptimistic } from 'react';

function CommentList({ comments }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state, newComment) => [
      ...state,
      { id: 'temp-' + Date.now(), text: newComment, pending: true }
    ]
  );

  async function addCommentAction(prevState, formData) {
    const text = formData.get('comment');
    addOptimisticComment(text);

    const res = await fetch('/api/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text }),
    });

    if (!res.ok) {
      return { error: '评论提交失败' };
    }
    return { success: true };
  }

  const [state, formAction, isPending] = useActionState(addCommentAction, null);

  return (
    <div>
      <ul>
        {optimisticComments.map(c => (
          <li key={c.id} className={c.pending ? 'pending' : ''}>{c.text}</li>
        ))}
      </ul>
      <form action={formAction}>
        <input type="text" name="comment" required />
        <button type="submit" disabled={isPending}>发表评论</button>
      </form>
    </div>
  );
}

Server Action与Next.js 15集成

React 19的Server Action允许直接在客户端组件中导入服务端函数。配合Next.js 15的App Router,表单提交可以绕过API路由层,直接在Server Action中操作数据库。

// app/login/actions.ts
'use server'

import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { compare } from 'bcryptjs'
import { z } from 'zod'
import { db } from '@/lib/db'

const loginSchema = z.object({
  email: z.string().email('邮箱格式不正确'),
  password: z.string().min(8, '密码至少8位'),
})

export async function loginAction(prevState, formData) {
  const result = loginSchema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
  })

  if (!result.success) {
    return { error: result.error.issues[0].message }
  }

  const { email, password } = result.data
  const user = await db.user.findUnique({ where: { email } })

  if (!user || !await compare(password, user.passwordHash)) {
    return { error: '邮箱或密码错误' }
  }

  const session = await createSession(user.id)
  cookies().set('session', session.token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
  })

  redirect('/dashboard')
}

// app/login/page.tsx
import { useActionState } from 'react'
import { loginAction } from './actions'

export default function LoginPage() {
  const [state, formAction, isPending] = useActionState(loginAction, null)

  return (
    <form action={formAction}>
      <input type="email" name="email" />
      <input type="password" name="password" />
      <button disabled={isPending}>
        {isPending ? '登录中' : '登录'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  )
}

Server Action的执行流程:表单提交触发POST请求到当前页面的RSC endpoint,Next.js在服务端执行loginAction函数,返回RSC payload更新客户端。客户端不接触密码或数据库连接,所有敏感操作在服务端完成。

表单校验策略

useActionState不排斥HTML原生表单校验。required、pattern、minlength等属性在表单提交前由浏览器执行首次校验,通过后才触发Action函数。配合Zod可实现在客户端快速预校验、服务端执行权威校验的双重保障。

const schema = z.object({
  username: z.string().min(3, '用户名至少3个字符').max(20),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(18, '必须年满18岁').max(120),
});

async function registerAction(prevState, formData) {
  const input = {
    username: formData.get('username'),
    email: formData.get('email'),
    age: Number(formData.get('age')),
  };

  const result = schema.safeParse(input);
  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }

  const res = await fetch('/api/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(result.data),
  });

  if (res.status === 409) {
    return { errors: { email: ['该邮箱已注册'] } };
  }
  return { success: true };
}

从React 18迁移到useActionState

迁移要点:

  • 将onSubmit handler改造为(prevState, formData) => newState的Action函数,移除preventDefault
  • 移除useState管理的error/isPending状态,改由useActionState返回值提供
  • 将form onSubmit改为form action,值为useActionState返回的formAction
  • 将手动读取e.target.elements.xxx.value改为formData.get(‘xxx’)

迁移前(React 18模式):

function OldForm() {
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsPending(true);
    setError(null);
    const formData = new FormData(e.target);
    const res = await fetch('/api/submit', { method: 'POST', body: formData });
    setIsPending(false);
    if (!res.ok) setError('提交失败');
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

迁移后(React 19模式):

function NewForm() {
  async function action(prevState, formData) {
    const res = await fetch('/api/submit', { method: 'POST', body: formData });
    if (!res.ok) return { error: '提交失败' };
    return { success: true };
  }

  const [state, formAction, isPending] = useActionState(action, null);
  return <form action={formAction}>...</form>;
}

代码行数减少约40%,state变量从3个降为0个。isPending由React管理,消除了手动管理异步状态导致的竞态条件风险。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/react19useactionstate-biao-dan-zhuang-tai-guan-li-yu-biao/

(0)
小编小编
上一篇 12小时前
下一篇 12小时前

相关推荐