Next.js表单中ColorPicker组件频繁重渲染问题求助
Next.js 表单组件重渲染问题解决
问题现象
开发Next.js表单时,每当操作ColorPicker输入框或上传图片,该组件都会触发不必要的重渲染。尝试过useCallback、React.memo、Ref等优化手段均未解决,移除addInputToState函数后组件恢复正常,推测问题根源在该函数内的setItems操作,但无法理解为何会触发ColorPicker重渲染。
相关代码
ColorPicker组件
"use client"; import { useCallback, useEffect, useState } from "react"; import SelectImage from "./SelectImage"; interface ColorPickerProps { variable: colorPickerProps; addInputToState: (value: colorPickerProps) => void; removeVariableFromState: (value: colorPickerProps) => void; isProductCreated: boolean; } const ColorPicker: React.FC<ColorPickerProps> = ({ addInputToState, removeVariableFromState, isProductCreated, variable, }: ColorPickerProps) => { const [isSelected, setIsSelected] = useState(false); const [File, setFile] = useState<File | null>(null); const [price, setPrice] = useState(0); const [Quantity, setQuantity] = useState(0); useEffect(() => { if (isProductCreated) { setIsSelected(false); setFile(null); } }, [isProductCreated]); const handleFileChange = useCallback( (value: File) => { setFile(value); addInputToState({ ...variable, image: value }); }, [addInputToState, variable] ); const handleInputChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; let Numbervalue = parseInt(value); if (name === "price") { if (!Numbervalue || Numbervalue < 0) Numbervalue = 0; if (Numbervalue > 1000000) Numbervalue = 1000000; setPrice(Numbervalue); addInputToState({ ...variable, price: Numbervalue }); } if (name === "stock") { if (!Numbervalue || Numbervalue < 0) Numbervalue = 0; if (Numbervalue > 1000) Numbervalue = 1000; setQuantity(Numbervalue); addInputToState({ ...variable, stock: Numbervalue }); } }, [addInputToState, variable] ); const handleCheck = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { setIsSelected(e.target.checked); if (!e.target.checked) { setFile(null); removeVariableFromState(variable); } }, [removeVariableFromState, variable] ); return ( <div className="flex flex-col gap-4 py-6 min-h-[5rem] text-center justify-center items-center rounded-md border-solid border-base-200 border-2 hover:border-base-content duration-500"> {/* ... 组件其余内容 ... */} </div> ); }; export default React.memo(ColorPicker);
完整表单组件
"use client"; import toast from "react-hot-toast"; import CategoriesCard from "./components/CategoriesCard"; import ColorPicker from "./components/ColorPicker"; import { currentUserType } from "@/utils/interfaces/types"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; import { v4 } from "uuid"; import { z } from "zod"; import { categories } from "./objects/categories"; import { variables } from "./objects/variables"; const variableSchema = z.object({ color: z.string(), colorCode: z.string(), stock: z.number(), price: z.number(), image: z.string(), }); const schema = z.object({ name: z.string(), description: z.string(), brand: z.string(), category: z.string(), variables: z.array(variableSchema), }); export interface colorPickerProps { color: string; colorCode: string; stock: number; price: number; image: File | null; } export default function AddMenu({ user }: { user: currentUserType | null }) { //Router Config const router = useRouter(); //Check if Authorized useEffect(() => { if (!user || user.role !== "ADMIN") { router.push("/"); toast.error("Acesso Negado"); return; } else if (user.role === "ADMIN") { setIsAuth(true); } }, [router, user]); //States const [isAuth, setIsAuth] = useState(false); const [items, setItems] = useState<colorPickerProps[]>([]); const [isCreated, setIsCreated] = useState(false); //Form Config const { register, handleSubmit, setValue, watch, reset, formState: { errors }, } = useForm({ resolver: zodResolver(schema) }); //Form Submit function onSubmit(data: FieldValues) {} //State to Form Data function removeVariableFromState(value: colorPickerProps) { setItems((prev) => { const filteredItems = prev?.filter((item) => item.color !== value.color); if (prev) { return filteredItems; } return prev; }); } const addInputToState = useCallback( (value: colorPickerProps) => { setItems((prevItems) => { if (!prevItems) { return [value]; } const foundItemIndex = prevItems.findIndex( (item) => item.color === value.color ); if (foundItemIndex === -1) { return [...prevItems, value]; } const newArray = [...prevItems]; newArray[foundItemIndex] = value; return newArray; }); }, [setItems] ); //set Custom Form Value const setCustomValue = useCallback( (id: string, value: any) => { setValue(id, value, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); }, [setValue] ); // Listen Changes Effects useEffect(() => { setCustomValue("variables", items); }, [items, setCustomValue]); useEffect(() => { if (isCreated) { reset(); setItems([]); setIsCreated(false); } }, [isCreated, reset]); //Watch Events const category = watch("category"); return ( <div className="w-3/4 h-3/4"> {isAuth && ( <form className="flex flex-col shadow-2xl rounded-xl my-4 mx-[13rem] py-8 px-[5rem] gap-5 " onSubmit={handleSubmit(onSubmit)} > <input className="rounded-md input border-base-300" placeholder="Nome do Produto" type="text" {...register("name")} /> <textarea className="textarea border-base-300" placeholder="Descrição do Produto" id="description" cols={10} rows={5} {...register("description")} ></textarea> <input className="rounded-md input border-base-300" placeholder="Marca do Produto" type="text" {...register("brand")} /> <div className="grid grid-cols-3 gap-4"> {categories.map(({ label, Icon }) => { return ( <CategoriesCard key={v4()} category={category} label={label} Icon={Icon} changeSelected={() => setCustomValue("category", label)} /> ); })} </div> <div className="grid grid-cols-2 justify-center items-center gap-6"> {variables.map((variable: colorPickerProps) => { return ( <ColorPicker key={v4()} variable={variable} removeVariableFromState={removeVariableFromState} addInputToState={addInputToState} isProductCreated={isCreated} /> ); })} </div> </form> )} </div> ); }
问题根源与解决方案
1. 核心问题:不稳定的key值
渲染ColorPicker时使用v4()生成随机key,每次父组件重渲染都会生成全新的key。React会将新key对应的组件视为全新组件,直接销毁旧组件并重建,完全绕过React.memo的浅比较优化,这是重渲染的主要原因。
修复:使用variable.color作为稳定key(保证每个ColorPicker的key唯一且不变):
{variables.map((variable: colorPickerProps) => { return ( <ColorPicker key={variable.color} // 替换v4()为稳定标识 variable={variable} removeVariableFromState={removeVariableFromState} addInputToState={addInputToState} isProductCreated={isCreated} /> ); })}
同理,CategoriesCard也建议用label作为稳定key,避免随机key导致的组件重建。
2. 未缓存的removeVariableFromState函数
removeVariableFromState没有用useCallback包裹,每次父组件重渲染都会生成新的函数引用。作为props传递给ColorPicker时,React.memo的浅比较会判定props变化,触发重渲染。
修复:用useCallback缓存该函数:
const removeVariableFromState = useCallback((value: colorPickerProps) => { setItems((prev) => { const filteredItems = prev?.filter((item) => item.color !== value.color); return filteredItems || prev; }); }, []); // setItems是useState返回的稳定函数,无需加入依赖
3. 优化addInputToState的依赖
setItems是useState返回的稳定函数,不需要作为useCallback的依赖,可以移除:
const addInputToState = useCallback( (value: colorPickerProps) => { setItems((prevItems) => { if (!prevItems) return [value]; const foundItemIndex = prevItems.findIndex(item => item.color === value.color); if (foundItemIndex === -1) return [...prevItems, value]; const newArray = [...prevItems]; newArray[foundItemIndex] = value; return newArray; }); }, [] // 移除setItems依赖 );
4. 确保variables对象引用稳定
如果variables数组中的对象是动态生成的,建议在组件外部定义或用useMemo缓存,避免每次渲染生成新对象导致React.memo判定props变化:
// 确保外部导入的variables对象引用稳定 import { variables } from "./objects/variables"; // 若为动态生成,用useMemo缓存 const variables = useMemo(() => [ /* 你的变量对象定义 */ ], []);
内容的提问来源于stack exchange,提问作者Kobra
相关产品推荐
相关产品推荐

