React useFieldArray场景下子组件传值到父组件并求和求助
解决方案
核心思路是把计算所需的coefficient和effort存入react-hook-form的表单字段中,而非子组件的本地状态,这样父组件可以通过watch实时获取所有动态字段的计算参数,自动处理索引变化,再统一求和。
1. 子组件(Requirement)修改
去掉本地的coefficient和effort状态,直接将这两个值绑定到表单字段,同时复用计算函数:
const Requirement = ({ hourValue, fieldId, errors, index, register, setCurrentIndex, requirementOptions, setValue, currentIndex, }: SelectProps) => { const requirementValueCalc = (effort: number, factor: number, hourValue: number) => { return effort * factor * hourValue; }; // 从表单字段获取当前项的coefficient和effort const getCurrentRequirementValues = () => { const coefficient = (register(`requirements.${index}.coefficient`).value as unknown as number) || 0; const effort = (register(`requirements.${index}.effort`).value as unknown as number) || 0; return { coefficient, effort }; }; return ( <> <div key={fieldId} className='flex flex-col gap-3'> <div className='flex flex-row items-center justify-start gap-10'> <div> <Input type={'checkbox'}></Input> </div> <div className='w-2/6'> {errors.requirements?.[index].title?.message && ( <span className='text-xs text-error-900'> {errors.requirements?.[index].title?.message} </span> )} <Input id='requirementTitle' placeholder='Título do requisito' withoutBorder {...register(`requirements.${index}.title`)} onFocus={() => setCurrentIndex(index)} className={classNames( errors.requirements ? 'border-error-900 px-3 py-2 placeholder-error-900 focus:border-error-900 focus:ring-error-900' : 'border-transparent px-3 py-2 placeholder-dark-300 focus:border-primary-500 focus:ring-0', 'block w-full appearance-none rounded-md border', 'bg-dark-900 text-white caret-primary-600', 'focus:outline-none sm:text-sm' )} /> </div> <div className='flex w-1/4 gap-3'> {requirementOptions.length > 0 && ( <> <Select handleSetValue={(value) => { // 同时设置typeRequirementId、coefficient、effort到表单字段 setValue(`requirements.${index}.typeRequirementId`, value.id); setValue(`requirements.${index}.coefficient`, value.coefficient); setValue(`requirements.${index}.effort`, value.effort); }} options={requirementOptions} /> <div className='flex ml-8 w-full gap-3'> {(() => { const { coefficient, effort } = getCurrentRequirementValues(); return <Title size={'bold'}>{`R$ ${requirementValueCalc(effort, coefficient, hourValue)}`}</Title>; })()} </div> </> )} </div> </div> <div className='mb-5 flex flex-col gap-2'> {errors.requirements?.[index].description?.message && ( <span className='text-xs text-error-900'> {errors.requirements?.[index].description?.message} </span> )} <textarea id='description' rows={2} placeholder='Descrição do requisito' className={classNames( errors.requirements ? 'border-error-900 px-3 py-2 placeholder-error-900 focus:border-error-900 focus:ring-error-900' : 'border-transparent px-3 py-2 placeholder-dark-300 focus:border-primary-500 focus:ring-0', 'block w-full appearance-none rounded-md border', 'bg-dark-900 text-white caret-primary-600', 'focus:outline-none sm:text-sm' )} {...register(`requirements.${index}.description`)} /> </div> <hr className={currentIndex ? 'border-dark-300 pb-5' : 'invisible'} /> </div> </> ); }; export { Requirement };
2. 父组件(ThirdPage)修改
使用watch监听整个requirements数组,实时计算总和并更新totalValue和totalHours:
import { watch } from 'react-hook-form'; const ThirdPage: ForwardRefRenderFunction< HTMLSelectElement, ThirdPageProps > = ({ formProps: { register, formState: { errors }, control, setValue, setFocus, watch, }, formData, secondPageData, }) => { const { dealId, object, contractPeriod, paymentMethodId, dueDay, totalHours, totalValue, } = formData[0]; const [currentIndex, setCurrentIndex] = useState<number>(0); const [requirements, setRequirements] = useState<RequerimentType[]>([]); const { append, remove, fields, insert } = useFieldArray({ name: 'requirements', control, }); // 监听requirements数组的变化 const formRequirements = watch('requirements', []); // 实时计算总和 const [calculatedTotalValue, setCalculatedTotalValue] = useState(totalValue || 0); const [calculatedTotalHours, setCalculatedTotalHours] = useState(totalHours || 0); useEffect(() => { const hourValue = secondPageData.hourValue; let newTotalValue = 0; let newTotalHours = 0; formRequirements.forEach(item => { const coeff = item.coefficient || 0; const effort = item.effort || 0; newTotalValue += effort * coeff * hourValue; newTotalHours += effort; }); setCalculatedTotalValue(newTotalValue); setCalculatedTotalHours(newTotalHours); // 更新表单字段 setValue('totalValue', newTotalValue); setValue('totalHours', newTotalHours); }, [formRequirements, secondPageData.hourValue, setValue]); const requirementOptions = requirements?.map( useCallback( (requirement) => { return { id: requirement.id, name: `${requirement.title} (${Math.floor( requirement.estimate / 8 )} dias)`, value: requirement.title, effort: requirement.estimate, coefficient: requirement.coefficient, }; }, [requirements] ) ); const handleKeyPress = (event: KeyboardEvent) => { if ( (event.ctrlKey || event.metaKey) && event.key === 'ArrowUp' && currentIndex > 0 ) { insert(currentIndex, {}); } if ((event.ctrlKey || event.metaKey) && event.key === 'ArrowDown') { insert(currentIndex + 1, {}); } if ( (event.ctrlKey || event.metaKey) && event.key === 'Backspace' && currentIndex > 0 ) { remove(currentIndex); setFocus(`requirements.${currentIndex - 1}.title`); } }; useEffect(() => { document.addEventListener('keydown', handleKeyPress); return () => { document.removeEventListener('keydown', handleKeyPress); }; }, [handleKeyPress]); useEffect(() => { const getAllRequirements = async () => { try { const response = await listAllRequirements(); setRequirements(response.data.items); } catch (err) { console.log(err.response.data); } }; getAllRequirements(); append({}); }, []); return ( <> <div className='flex flex-col divide-y divide-zinc-700 divide-opacity-25 px-6 last:border-0 sm:px-2'> <div className='flex flex-col'> <div className='flex flex-row items-center justify-between gap-10'> <Input placeholder='Id da oportunidade vem do RD' label='Id da oportunidade' withoutBorder error={errors.dealId} {...register('dealId')} defaultValue={dealId} /> <Input placeholder='Objeto' label='Objeto' withoutBorder error={errors.object} {...register('object')} defaultValue={object} /> </div> <div className='flex flex-row items-center justify-between gap-10 pt-6'> <Input placeholder='Período do contrato' label='Período do contrato' withoutBorder error={errors.contractPeriod} {...register('contractPeriod')} defaultValue={contractPeriod} /> <Input placeholder='Método de pagamento' label='Método de pagamento' withoutBorder error={errors.paymentMethodId} {...register('paymentMethodId')} defaultValue={paymentMethodId} disabled /> </div> <div className='flex items-center justify-between gap-10 pt-6'> <Input placeholder='Dia de vencimento da parcela' label='Dia de vencimento da parcela' type='number' withoutBorder error={errors.dueDay} {...register('dueDay')} defaultValue={dueDay} /> <div className='flex w-full flex-row items-center justify-between gap-10'> <div className='flex w-full justify-end'> <input label='Total de horas' id='totalHours' type='number' hidden error={errors.totalHours} {...register('totalHours')} defaultValue={calculatedTotalHours} /> </div> <div className='flex w-full flex-col items-start gap-2'> <input id='totalValue' hidden={true} defaultValue={calculatedTotalValue} {...register('totalValue')} /> </div> </div> </div> <hr className='mt-8 border-dark-300 pt-5' /> <div className='flex w-full justify-between'> <h1 className='font-bold text-primary-600'>Requisitos</h1> <Tooltip /> </div> {fields.map((field, index) => ( <Requirement fieldId={field.id} errors={errors} index={index} register={register} setCurrentIndex={setCurrentIndex} requirementOptions={requirementOptions} setValue={setValue} currentIndex={currentIndex} hourValue={secondPageData.hourValue} /> ))} <div className='flex w-full justify-end'> <div className='flex-col'> <div className='flex justify-end'> <h6 className='py-2 text-white'>{`${calculatedTotalHours} / horas`}</h6> </div> <div className='flex justify-end'> <Title size={'bold'}>{`R$ ${calculatedTotalValue}`}</Title> </div> <div className='flex justify-end'> <h6 className='py-2 text-white'> <b>4 sprint</b> / ORÇADO </h6> </div> <div className='flex justify-end'> <h6 className='py-2 text-white'> <b>{`${Math.floor(calculatedTotalHours / 8)} dias úteis`}</b> / CONTRATO </h6> </div> </div> </div> </div> </div> </> ); }; export default ThirdPage;
为什么这样能解决索引问题?
- react-hook-form的
useFieldArray会自动维护动态字段的索引和唯一标识,watch获取到的数组始终是最新结构,包含所有字段的当前值。 - 不需要手动追踪子组件的索引变化,所有计算依赖表单数据源,天然适配字段的添加、删除、插入操作。
- 子组件不再依赖本地状态,数据完全由表单管理,避免了父子组件状态不同步的问题。
内容的提问来源于stack exchange,提问作者Sabrynna
相关产品推荐
相关产品推荐

