Next.js 13中setState致输入字段更新失效的调试求助
问题根源与修复方案
你的问题核心是受控组件状态管理混乱,加上组件重渲染时输入框被强制重置,导致首次操作后失效。具体问题点和修复步骤如下:
1. 输入框硬编码value={0}导致重渲染后重置
你给输入框写了value={0},每次调用setBuyFormData触发组件重渲染时,输入框都会被强制设回0。react-hook-form的受控输入应该用form提供的状态,而非硬写固定值。
修复:通过watch获取当前字段值作为输入框的value,确保完全受控。
2. 直接操作DOM元素的e.value违反React原则
你在handleTicketChange里手动修改DOM的e.value,这会和react-hook-form的状态同步冲突。React里应该完全通过状态管理来更新输入值,不要直接碰DOM。
修复:用react-hook-form的watch方法获取当前字段值,计算后用setValue更新,彻底移除对DOM的直接操作。
3. 重复获取useFormContext上下文
你分别用readFormData和后面的解构获取了两次useFormContext,冗余且易引发误解。统一成一次获取即可。
4. formFields无需用useState
formFields是静态数据,不需要放到useState里,直接定义为常量,避免不必要的组件重渲染触发条件。
修改后的完整代码
'use client'; import React from 'react'; import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; import { useState } from 'react'; import { FieldValues, useFormContext } from 'react-hook-form'; import TicketSummaryCard from './TicketSummaryCard'; //Typing type EleventlyEvent = { event: Event; event_title: string; }; // 静态数据直接定义为常量,不用useState const formFields = [ { id: '0', ticketName: 'Régulier', description: 'Régulier', price: '40.00', limit: '5' }, { id: '1', ticketName: 'Admission', description: 'Ceci est une description Admission', price: '10.00', limit: '10' }, { id: '2', ticketName: 'VIP', description: 'Ceci est une description VIP', price: '80.00', limit: '1' }, { id: '3', ticketName: 'ENFANT', description: 'Voici le billet pour enfant', price: '25.00', limit: '2' } ]; function TicketSelection({ event }: EleventlyEvent) { // 统一获取form上下文 const { register, setValue, watch, getValues, formState: { errors } } = useFormContext(); const [buyformData, setBuyFormData] = useState<{ ticketName: string; ticketPrice: number; ticketNumber: number; }[]>([]); // 监听form值变化,自动更新buyformData,替代手动调用updateBuyList React.useEffect(() => { const subscription = watch((values) => { updateBuyList(values); }); return () => subscription.unsubscribe(); }, [watch]); const handleTicketChange = (ticketName: string, action: string) => { // 用watch获取当前值,而非DOM const currentValue = watch(ticketName) || 0; const ticket = formFields.find(f => f.ticketName === ticketName); if (!ticket) return; const buyLimit = parseInt(ticket.limit); let updatedValue: number; switch (action) { case 'plus': if (currentValue < buyLimit) { updatedValue = currentValue + 1; setValue(ticketName, updatedValue); } break; case 'minus': if (currentValue > 0) { updatedValue = currentValue - 1; setValue(ticketName, updatedValue); } break; default: return; } }; const updateBuyList = (tempBuyList?: FieldValues) => { // 优先用传入的values,没有则手动获取 const values = tempBuyList || getValues(); let updatedValues: typeof buyformData = []; Object.keys(values).forEach(key => { const formField = formFields.find(f => f.ticketName === key); if (!formField) return; const ticketNumber = values[key]; if (ticketNumber) { updatedValues.push({ ticketName: key, ticketPrice: parseFloat(formField.price), ticketNumber: ticketNumber }); } }); setBuyFormData(updatedValues); }; return ( <> <main className="bg-white"> <div className="mx-auto max-w-8xl px-4 sm:px-2"> <div className="py-12 grid max-w-2xl grid-cols-1 grid-rows-1 items-start gap-x-8 gap-y-8 lg:mx-0 lg:max-w-none lg:grid-cols-12"> <div className="col-span-7 lg:col-span-9 lg:col-start-1 lg:row-end-1"> <h1 className="text-black font-semibold text-3xl"> Selection de billets </h1> <div className=""> {formFields.map((item) => { // 用watch获取当前输入值 const currentValue = watch(item.ticketName) || 0; return ( <div className="rounded-lg bg-white shadow-sm ring-1 ring-gray-900/5 my-10" key={item.id} // 用唯一id当key,避免列表重排序渲染问题 > <dl className="flex flex-wrap"> <div className="flex-auto pl-6 pt-6"> <span className="text-base font-semibold leading-6 text-gray-900"> {item.ticketName} </span> </div> <div className="flex-none self-end px-6 pt-4"> <div className="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center"> <div className="flex flex-row"> <button type="button" onClick={() => handleTicketChange(item.ticketName, 'minus')} className="rounded-sm bg-[#33DEBD] p-1.5 text-black shadow-sm hover:bg-primaryhover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600" > <MinusIcon className="h-3 w-3 rounded" aria-hidden="true" /> </button> <input className="mx-auto w-12 p-0 m-0 text-center border-0 text-gray-900 bg-transparent font-semibold" id={item.id} data-limit={item.limit} value={currentValue} // 用watch来的当前值,受控 {...register(item.ticketName)} /> <button type="button" onClick={() => handleTicketChange(item.ticketName, 'plus')} className="rounded-sm bg-[#33DEBD] p-1.5 text-black shadow-sm hover:bg-primaryhover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600" > <PlusIcon className="h-3 w-3 rounded" aria-hidden="true" /> </button> </div> </div> </div> </dl> <div className="mt-6 border-t border-gray-900/5 px-6 py-6"> <dt className="text-md font-semibold leading-6 text-gray-900"> {item.price} </dt> <dl className="leading-6 font-normal text-gray-500"> {item.description} </dl> </div> </div> ) })} </div> </div> {/* Ticket Price summary */} <div className="col-span-7 lg:col-span-7 lg:row-end-1 "> <TicketSummaryCard event={event} buylist={buyformData} /> </div> </div> </div> </main> </> ); } export default TicketSelection;
额外优化点
- 把
formFields的key从index改成item.id,避免列表重排序时的渲染异常 - 用
watch的订阅替代手动调用updateBuyList,任何表单值变化都会自动同步到buyformData - 把
ticketPrice从字符串转成数字,方便子组件计算总价
内容的提问来源于stack exchange,提问作者Maken
相关产品推荐
相关产品推荐

