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
相关产品推荐
相关产品推荐

