React Hook Form中非提交按钮点击致组件多次重渲染求助
问题解决:React Hook Form表单内按钮点击导致多次重渲染
问题现象
点击表单内的「Add banner to list」按钮时,组件频繁重渲染(控制台打印boo约20次);将按钮移到表单外则无此问题,但业务需要在按钮点击时调用setBannersDataList且必须放在表单内。
代码复现
'use client'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Button } from './ui/button'; import { SubmitHandler, useForm } from 'react-hook-form'; import { Form, FormControl, FormField, FormItem, FormLabel } from './ui/form'; import { useState } from 'react'; type EditorProps = { details: { sizes?: string[] }; }; export default function Component({ details }: EditorProps) { const [bannersDataList, setBannersDataList] = useState<any>([]); const onSubmit: SubmitHandler<any> = (data) => console.log(data); const form = useForm<any>(); const { register, setValue, handleSubmit, watch, getValues, formState: { errors }, } = form; console.log('boo'); return ( <Form {...form}> <form className="grid w-full items-start gap-6" onSubmit={handleSubmit(onSubmit)}> <FormField control={form.control} name="size" render={({ field }) => ( <FormItem> <FormLabel>Sizes</FormLabel> <FormControl> <Select {...field} defaultValue="300x250" > <SelectTrigger id="size" className="items-start [&_[data-description]]:hidden"> <SelectValue placeholder="Select a size" /> </SelectTrigger> <SelectContent> {details.sizes?.map((size) => ( <SelectItem value={size} key={size}> {size} </SelectItem> ))} </SelectContent> </Select> </FormControl> </FormItem> )} /> <Button onClick={() => { // 点击此处触发多次重渲染 setBannersDataList(prev => [...prev, getValues()]); }} > Add banner to list </Button> </form> </Form> ); }
解决方案
1. 阻止按钮默认表单提交行为
表单内的<button>默认类型是submit,点击会触发表单提交,进而触发React Hook Form内部状态更新,导致多次重渲染。给按钮加上type="button"即可避免:
<Button type="button" onClick={() => { setBannersDataList(prev => [...prev, getValues()]); }} > Add banner to list </Button>
也可以在回调里手动阻止默认行为:
<Button onClick={(e) => { e.preventDefault(); setBannersDataList(prev => [...prev, getValues()]); }} > Add banner to list </Button>
2. 优化表单订阅逻辑
当前代码中watch()未指定具体字段,会订阅所有表单字段的变化,任何值变动都会触发组件重渲染。如果不需要监听全部字段,只订阅需要的目标字段:
// 仅监听size字段,而非所有表单值 const size = watch('size');
如果完全不需要watch功能,直接移除该解构项,减少不必要的订阅。
3. 稳定回调函数引用
用useCallback包裹按钮点击回调,避免每次组件渲染都创建新函数实例,防止子组件(Button)不必要的重渲染:
import { useCallback } from 'react'; // ... const handleAddBanner = useCallback(() => { setBannersDataList(prev => [...prev, getValues()]); }, [getValues]); // ... <Button type="button" onClick={handleAddBanner}> Add banner to list </Button>
4. 缓存外部依赖项
如果父组件传递的details每次渲染都是新对象(比如直接在父组件中创建{ sizes: [...] }),会导致当前组件频繁重渲染。用useMemo缓存sizes数组:
import { useMemo } from 'react'; // ... const sizes = useMemo(() => details.sizes || [], [details.sizes]); // ... <SelectContent> {sizes.map((size) => ( <SelectItem value={size} key={size}> {size} </SelectItem> ))} </SelectContent>
5. 调整useForm初始化配置
显式声明shouldUnregister: false(默认值即为false,显式声明可避免潜在配置问题),避免字段被频繁卸载/注册导致的重渲染:
const form = useForm<any>({ shouldUnregister: false, });
内容的提问来源于stack exchange,提问作者Amit Kapadia
相关产品推荐
相关产品推荐

