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.
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.