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

React 19+NextJS中useFormState表单重置及弹窗二次打开问题

问题解决方案

问题分析

  1. 表单错误未重置: 关闭弹窗后再次打开,之前的验证错误依然显示。原因是useFormState保留了之前的状态,且代码中直接修改了不可变的state对象(state.message = ''),这是无效操作。
  2. 提交成功后无法重新打开弹窗: 提交成功后,点击"Add card"按钮无法打开弹窗。原因是原useEffect监听state和open,当弹窗重新打开时,state仍处于success状态,触发setOpen(false)导致弹窗立即关闭。

解决方案

核心修复点

  1. 通过key重置表单状态: 当弹窗关闭时,修改表单的key属性,强制React重新挂载表单组件,从而重置useFormState到初始状态。
  2. 移除无效的状态修改: 不要直接修改useFormState返回的state对象,它是不可变的。
  3. 修正弹窗关闭逻辑: 移除导致弹窗自动关闭的错误useEffect,改为在提交成功后关闭弹窗,并通过表单key重置状态。
  4. 修复错误提示的 typo: 价格字段的错误提示引用了hours的错误,改为price。

修正后的CreateCardDialog组件代码

'use client'

import { createCard } from '@/app/u/cards/actions'
import SubmitButton from '@/components/submitbutton'
import { Button } from '@/components/ui/button'
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip'
import { Tables } from '@/types/supabase'
import { InfoCircledIcon } from '@radix-ui/react-icons'
import { Euro } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { useFormState } from 'react-dom'

type CreateClientDialogProps = {
  children: React.ReactNode
  clients: Tables<'clients'>[] | null
}

const initialState = undefined

const CreateCardDialog = ({ children, clients }: CreateClientDialogProps) => {
  const [open, setOpen] = useState(false)
  const formRef = useRef<HTMLFormElement>(null)
  const [state, formAction] = useFormState(createCard, initialState)
  const [customEndDate, setCustomEndDate] = useState(false)
  // 用于重置表单的key,每次弹窗关闭时递增
  const [formKey, setFormKey] = useState(0)

  const handleSwitchChange = () => {
    setCustomEndDate(!customEndDate)
  }

  const today = new Date()
  const oneYearFromNow = new Date(
    today.getFullYear() + 1,
    today.getMonth(),
    today.getDate()
  )
  const formattedDate = oneYearFromNow.toISOString().split('T')[0]

  // 提交成功后关闭弹窗
  useEffect(() => {
    if (state?.status === 'success') {
      setOpen(false)
    }
  }, [state])

  return (
    <>
      {open ? <p className='text-lg'>JA</p> : <p className='text-lg'>NEE</p>}
      <Dialog 
        open={open} 
        onOpenChange={(newOpen) => {
          setOpen(newOpen)
          // 弹窗关闭时,递增formKey重置表单状态
          if (!newOpen) {
            setFormKey(prev => prev + 1)
          }
        }}
      >
        <DialogTrigger asChild>{children}</DialogTrigger>
        <DialogContent>
          <p>
            {clients && clients.length === 0 ? (
              <div>
                Please add
                <Link href='/u/clients' className='border-b border-b-black'>
                  clients
                </Link>
                first before you add a card.
              </div>
            ) : (
              // 添加key属性,每次弹窗关闭时重新挂载表单
              <form ref={formRef} action={formAction} key={formKey}>
                <div className='mb-4'>
                  <Label htmlFor='client_id' className='mb-2'>
                    Client
                  </Label>
                  <Select name='client_id' required>
                    <SelectTrigger className='w-[240px]'>
                      <SelectValue placeholder='Select client' />
                    </SelectTrigger>
                    <SelectContent>
                      {clients?.map((client) => (
                        <SelectItem key={client.id} value={client.id}>
                          {client.name}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                  {state?.errors?.client_id && (
                    <p className='py-2 text-xs text-red-500'>
                      {state.errors.client_id}
                    </p>
                  )}
                </div>
                <div className='mb-4'>
                  <Label htmlFor='hours' className='mb-2'>
                    Hours
                  </Label>
                  <Input type='number' name='hours' id='hours' required />
                  {state?.errors?.hours && (
                    <p className='py-2 text-xs text-red-500'>
                      {state.errors.hours}
                    </p>
                  )}
                </div>
                <div className='mb-4'>
                  <Label htmlFor='price' className='mb-2'>
                    Price
                  </Label>
                  <div className='relative flex items-center max-w-2xl '>
                    <Euro className='absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 transform' />
                    <Input
                      type='number'
                      name='price'
                      id='price'
                      required
                      className='pl-6'
                    />
                    {/* 修复typo:从hours改为price */}
                    {state?.errors?.price && (
                      <p className='py-2 text-xs text-red-500'>
                        {state.errors.price}
                      </p>
                    )}
                  </div>
                </div>
                <div className='mb-4 flex flex-col'>
                  <div className='flex items-center'>
                    <Switch
                      checked={customEndDate}
                      className='mr-2'
                      onCheckedChange={handleSwitchChange}
                    />
                    <span className=''>Set custom end date</span>
                    <TooltipProvider>
                      <Tooltip>
                        <TooltipTrigger asChild>
                          <InfoCircledIcon className='ml-1' />
                        </TooltipTrigger>
                        <TooltipContent>
                          <p>By default cards are valid for one year</p>
                        </TooltipContent>
                      </Tooltip>
                    </TooltipProvider>
                  </div>
                  {customEndDate ? (
                    <>
                      <Label htmlFor='ends_at' className='my-2 mr-2'>
                        Valid until
                      </Label>
                      <input
                        aria-label='Date'
                        type='date'
                        id='ends_at'
                        name='ends_at'
                        required
                        defaultValue={formattedDate}
                      />
                    </>
                  ) : (
                    <input
                      type='hidden'
                      aria-label='Date'
                      id='ends_at'
                      name='ends_at'
                      required
                      defaultValue={formattedDate}
                    />
                  )}
                </div>
                <p aria-live='polite' className='sr-only'>
                  {state?.message}
                </p>
                <div className='mb-4'>
                  {/* 直接使用state中的message,无需额外状态 */}
                  {state?.status === 'error' && state?.message && (
                    <p className='py-2 text-xs text-red-500'>{state.message}</p>
                  )}
                </div>
                <DialogClose asChild>
                  <Button variant='outline' className='mr-2'>
                    Cancel
                  </Button>
                </DialogClose>
                <SubmitButton normal='Add card' going='Adding  card...' />
              </form>
            )}
          </p>
        </DialogContent>
      </Dialog>
    </>
  )
}

export default CreateCardDialog

关键改进说明

  • 表单重置: 通过formKey实现表单的重新挂载,确保每次打开弹窗时useFormState都从初始状态开始,解决了错误残留的问题。
  • 状态处理: 移除了对state对象的直接修改,改为依赖React的状态更新机制,避免无效操作。
  • 弹窗逻辑: 修正了提交成功后弹窗无法重新打开的问题,移除了导致弹窗自动关闭的错误useEffect,改为在提交成功后主动关闭弹窗,并通过formKey重置状态。
  • 错误提示: 修复了价格字段错误提示的引用错误,确保显示正确的验证信息。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 01:32:02