Search code examples
reactjsformsrenderingnext.js13

Component Re-rendering on Next.js Form


Basically my problem is, every time I add a value to the Color Picker input or add an image, it re-renders, I'm stuck with this for days.

// ColorPicker component

"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">
      {/* ... rest of the component ... */}
    </div>
  );
};

export default React.memo(ColorPicker);

The entire Form Component:

"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>
  );
}

I tried using Callbacks, Memo, Ref, everything, but I think the problem is in the logic of it.I deleted the addInputtoState and work normally, so I think the root of it is the setItem inside of it, but I don't understando why changing it re-renders the ColorPicker.


Solution

  • I believe there is some confusion in this question. Re-renders are not the same as "component unmounts completely and remounts". From your code, it seems you are having issues with the latter.

    Actual re-renders are completely normal and a fact of using React. It looks like you have tried to prevent re-renders, but what you actually have is a bug of an entirely different nature.

    The problem is the misuse of the key prop passed to ColorPicker.

    Every time the parent, AddMenu re-renders, which it will do when its state changes (note: necessarily, that can't be prevented), you are regenerating a key via the v4() call. This is a random generator so that key will be different every time the JSX executes (a "re-render").

    If a key changes between renders, then that instance of the component will be totally unmounted and mounted again, losing all its state in the process. This is the real problem, not the re-render itself, which is usually harmless. Keys must be stable, and not random. That means they are predictably the same every time this component executes a re-render cycle. The docs state:

    Similarly, do not generate keys on the fly, e.g. with key={Math.random()}. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list of items. Instead, use a stable ID based on the data.

    Usually, this would be achieved by ensuring each entry of variables has a unique id or similar. In the objects/variables file, you should add such a property (assuming none of the existing ones uniquely represents each item already). The value for each entry must be unique.

    Then you would use this as the key:

     {variables.map((variable: colorPickerProps) => {
                  return (
                    <ColorPicker
                      key={variable.id}
                      variable={variable}
                      removeVariableFromState={removeVariableFromState}
                      addInputToState={addInputToState}
                      isProductCreated={isCreated}
                    />
                  );
                })}
    

    Please note that you won't need the React.memo wrapper. Such techniques are for reducing re-renders (by the strict definition of what a re-render is) when there's a performance problem. But your case does not appear to be this. Arguably it might be overkill on all the useCallbacks and useMemo's also, but your mileage may vary. They are solving problems that likely don't exist for you yet.