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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 04:20:55