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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 07:02:05