You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13 App Router中Form Action内setState更新时机异常

问题

使用Next.js 13.5 App Router开发联系表单时,遇到异常现象:把提交逻辑写在form的action函数中时,setIsLoading(true)不会在点击提交按钮时立即触发,而是要等到server action(sendEmail)执行完成后才会更新状态。但将提交逻辑改为按钮的onClick函数后,setIsLoading能正常在点击时立即设为true,邮件发送完成后恢复为false。

相关代码:

"use client"
import { sendEmail } from '@/features/sendEmail'

export const Contact() => {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState('')

  const formAction  = async () => {
    try {
        setIsLoading(true) // 此代码与sendEmail执行结束同时触发
        const result = await sendEmail(formData) // 该函数为server action
        result.success ? router.push("thanks") : setError("server error")
      } catch (error) {
        setServerError(`${error}`)
        setIsLoading(false)
      }
    }
  return (
    <form action={formAction}>
      {/* 一些输入框 */}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'sending' : 'send'}
      </button>
    </form>
  )
}

改写后的onClick逻辑:

const submitFunc = async () => {
  setIsLoading(true)
  const result = await sendEmail(formData) // 该函数为server action
  setIsLoading(false)
}

原因分析

这是Next.js App Router中表单action的特殊执行机制导致的:

  • 当把async函数绑定为form的action时,Next.js会将整个表单提交流程纳入自身处理生命周期,包括自动序列化表单数据、处理server action的调用。
  • 在此流程中,客户端组件内的状态更新(比如setIsLoading(true))会被React的批处理更新或Next.js内部机制延迟,直到server action响应返回后才会批量执行状态更新,因此看起来setIsLoading和server action结束同时触发。
  • 而onClick是直接在客户端触发同步状态更新,之后再异步调用server action,不受Next.js表单action的生命周期限制,因此状态能立即更新。

解决方案

方案1:使用官方推荐的useFormStatus钩子

Next.js为表单action场景提供了专门的useFormStatus钩子来获取表单加载状态,这是最符合官方规范的做法:

"use client"
import { sendEmail } from '@/features/sendEmail'
import { useFormStatus } from 'react-dom'

// useFormStatus必须在form的子组件中使用,因此单独抽离提交按钮
const SubmitButton = () => {
  const { pending } = useFormStatus()
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'sending' : 'send'}
    </button>
  )
}

export const Contact() => {
  const [error, setError] = useState('')

  const formAction  = async (formData) => {
    try {
        const result = await sendEmail(formData)
        result.success ? router.push("thanks") : setError("server error")
      } catch (error) {
        setError(`${error}`)
      }
    }
  return (
    <form action={formAction}>
      {/* 一些输入框 */}
      <SubmitButton />
    </form>
  )
}

方案2:保留onClick逻辑

如果更习惯用onClick的方式,需手动处理表单默认提交行为并获取表单数据:

"use client"
import { sendEmail } from '@/features/sendEmail'
import { useRef, useState } from 'react'

export const Contact() => {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState('')
  const formRef = useRef(null)

  const submitFunc = async (e) => {
    e.preventDefault()
    setIsLoading(true)
    try {
      const formData = new FormData(formRef.current)
      const result = await sendEmail(formData)
      result.success ? router.push("thanks") : setError("server error")
    } catch (error) {
      setError(`${error}`)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <form ref={formRef} onSubmit={submitFunc}>
      {/* 一些输入框 */}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'sending' : 'send'}
      </button>
    </form>
  )
}

方案3:用flushSync强制立即更新状态

如果一定要用form action的方式,可通过React的flushSync强制立即执行状态更新:

"use client"
import { sendEmail } from '@/features/sendEmail'
import { flushSync, useState } from 'react'

export const Contact() => {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState('')

  const formAction  = async (formData) => {
    try {
        flushSync(() => {
          setIsLoading(true)
        })
        const result = await sendEmail(formData)
        result.success ? router.push("thanks") : setError("server error")
      } catch (error) {
        setError(`${error}`)
      } finally {
        flushSync(() => {
          setIsLoading(false)
        })
      }
    }
  return (
    <form action={formAction}>
      {/* 一些输入框 */}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'sending' : 'send'}
      </button>
    </form>
  )
}

内容的提问来源于stack exchange,提问作者isamu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 05:46:40