In this project (React with NextJS), the user can see various statistics, statistics like how many orders a certain rider made, how many hours it worked and so on. The user is not limited to just one “section”, they can add and remove as many sections as they wants so that they compare, for example, the stats of a certain rider vs another rider. The code here represents the page where these sections are shown. The key is that I have a useState containing an array of “Stats” nodes (the dialogs I talked about):
export default function Layout({ riders }: { riders: Rider[] }) {
const [results, setResults] = useState<
{ index: number; data: StatsType[] }[]
>([]);
function onResult(result: StatsType[], index: number) {
setResults((prevResults) => {
const existingResultIndex = prevResults.findIndex(
(item) => item.index === index
);
if (existingResultIndex !== -1) {
return prevResults.map((item, i) =>
i === existingResultIndex ? { ...item, data: result } : item
);
} else {
return [...prevResults, { index, data: result }];
}
});
}
const [stats, setStats] = useState<ReactNode[]>([
<Stats key={0} riders={riders} onResult={onResult} index={0} />,
]);
const handleAddStats = () => {
const newStats = [
...stats,
<Stats
riders={riders}
onResult={onResult}
index={stats.length}
key={stats.length}
/>,
];
setStats(newStats);
};
const handleRemoveStats = (indexToRemove: number) => {
if (stats.length > 1) {
const newStats = stats.filter((_, index) => index !== indexToRemove);
setStats(newStats);
setResults((prevResults) => {
const updatedResults = prevResults.filter(
(item) => item.index !== indexToRemove
);
return updatedResults.map((result, index) => ({
...result,
index: index,
}));
});
}
};
return (
<div className="w-full flex flex-col gap-12 items-center justify-center">
<div>
<h1 className="text-4xl mt-8 w-full text-center">Statistiche</h1>
{results && results.length !== 0 && <GraphDialog results={results} />}
</div>
<div className="flex w-full flex-wrap gap-y-8 justify-between">
{stats.map((stat, index) => (
<div className="relative w-[49%] group" key={index}>
{stat}
<X
onClick={() => handleRemoveStats(index)}
size={36}
className="absolute top-[-1rem] right-[-1rem] invisible
group-hover:visible hover:cursor-pointer hover:bg-opacity-50 hover:bg-white rounded-full p-1"
/>
</div>
))}
<div
className={`${
stats.length % 2 === 0 ? "w-full" : "w-[49%]"
} flex items-center justify-center`}
>
<Button className="" onClick={handleAddStats}>
<Plus className="mr-2 h-4 w-4" />
Aggiungi statistica
</Button>
</div>
</div>
</div>
);
}
And not that Stats component:
export default function Stats({
riders: receivedRiders,
index,
onResult,
}: {
riders: Rider[];
index: number
onResult: (result: StatsType[], index: number) => void;
}) {
const riders = receivedRiders;
const [rider, setRider] = useState<string>("all");
const [date, setDate] = useState<DateRange>();
const [context, setContext] = useState<string>("all");
function handlePresetSelect(value: string) {
switch (value) {
case "today":
setDate({ from: new Date(), to: new Date() });
break;
case "yesterday":
setDate({ from: subDays(new Date(), 1), to: subDays(new Date(), 1) });
break;
case "last7":
setDate({ from: subDays(new Date(), 6), to: new Date() });
break;
case "last30":
setDate({ from: subDays(new Date(), 29), to: new Date() });
break;
case "thisMonth":
setDate({ from: startOfMonth(new Date()), to: endOfMonth(new Date()) });
break;
case "thisYear":
setDate({ from: startOfYear(new Date()), to: endOfYear(new Date()) });
break;
default:
break;
}
}
return (
<div className="flex flex-col items-center p-6 w-[100%] gap-8 border- border rounded-lg">
<div>{index}</div>
<div className="flex items-center gap-8 w-full">
<Select onValueChange={setRider} defaultValue="all">
<div className="space-y-2 w-1/3">
<Label htmlFor="rider">Chi?</Label>
<SelectTrigger id="rider">
<SelectValue placeholder="Seleziona un ragazzo" />
</SelectTrigger>
</div>
<SelectContent id="rider">
<SelectItem key={0} value={"all"} defaultChecked={true}>
Tutti
</SelectItem>
{riders.map((rider) => (
<SelectItem
key={rider.id}
value={rider.id.toString() + "-" + rider.nickname}
>
{rider.name + " " + rider.surname + " ("}
<strong>{rider.nickname}</strong>)
</SelectItem>
))}
</SelectContent>
</Select>
<div className="space-y-2 w-1/2">
<Label htmlFor="date">Data</Label>
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
date.from && date.to ? (
`${format(date.from, "PPP", {
locale: it,
})} - ${format(date.to, "PPP", { locale: it })}`
) : (
<span>Seleziona la data</span>
)
) : (
<span>Seleziona la data</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-auto flex-col space-y-2 p-2">
<Select
onValueChange={(value) => {
handlePresetSelect(value);
}}
>
<SelectTrigger>
<SelectValue placeholder="Date veloci" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="today">Oggi</SelectItem>
<SelectItem value="yesterday">Ieri</SelectItem>
<SelectItem value="last7">Ultimi 7 giorni</SelectItem>
<SelectItem value="last30">Ultimi 30 giorni</SelectItem>
<SelectItem value="thisMonth">Questo mese</SelectItem>
<SelectItem value="thisYear">Questo'anno</SelectItem>
</SelectContent>
</Select>
<div className="rounded-md border">
<Calendar
locale={it}
mode="range"
selected={date}
onSelect={setDate}
numberOfMonths={1}
//onDayTouchStart={(e) => console.log(e)}
/>
</div>
</PopoverContent>
</Popover>
</div>
<Select onValueChange={setContext} defaultValue="all">
<div className="space-y-2 w-1/3">
<Label htmlFor="context">Cosa?</Label>
<SelectTrigger id="context">
<SelectValue placeholder="Seleziona un contesto" />
</SelectTrigger>
</div>
<SelectContent>
<SelectItem key={1} value={"all"} defaultChecked={true}>
Tutto
</SelectItem>
<SelectItem key={2} value={"orders"}>
Consegne
</SelectItem>
<SelectItem key={3} value={"time"}>
Ore
</SelectItem>
<SelectItem key={4} value={"money"}>
Incassi
</SelectItem>
</SelectContent>
</Select>
</div>
{rider !== "all" && date?.from && date?.to && (
<StatsResult
riderId={parseInt(rider.split("-")[0])}
date={date}
index={index}
context={context}
isAllRiders={false}
onResult={onResult}
/>
)}
{rider === "all" && date?.from && date?.to && (
<StatsResult
date={date}
index={index}
context={context}
isAllRiders={true}
onResult={onResult}
/>
)}
</div>
);
}
Stats uses “StatsResult”, which is the component that will actually shown the results (just a small table with the results)
export default function StatsResult({
index,
riderId,
date,
context,
isAllRiders,
onResult,
}: {
index: number;
riderId?: number;
date: DateRange | undefined;
context: string;
isAllRiders: boolean;
onResult: (result: StatsType[], index: number) => void;
}) {
const [result, setResult] = useState<StatsType[]>();
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
setLoading(true);
const body = isAllRiders
? { date, context, isAllRiders: true }
: { riderId, date, context, isAllRiders: false };
fetch("/api/stats/get", {
method: "POST",
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
response.json().then((result) => {
setResult(result);
onResult(result, index);
setLoading(false);
});
}
});
}, [riderId, date, context, isAllRiders]);
return (
<div className="w-full overflow-y-auto max-h-[500px]">
{date && result && result.length !== 0 ? (
<Table className="w-full text-2xl">
<TableHeader className="sticky top-0 z-10 bg-background">
<TableRow>
{result.some((item) => item.riderName !== undefined) && (
<TableHead className="w-[25%]">Ragazzo</TableHead>
)}
{result.some((item) => item.totalOrders !== undefined) && (
<TableHead className="w-[25%]">Consegne</TableHead>
)}
{result.some((item) => item.totalHours !== undefined) && (
<TableHead className="w-[25%]">Ore</TableHead>
)}
{result.some((item) => item.totalMoney !== undefined) && (
<TableHead className="w-[25%]">Guadagno</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{result.map((item, index) => (
<TableRow key={index}>
<TableCell>{item.riderName}</TableCell>
{item.totalOrders !== undefined && (
<TableCell>{item.totalOrders}</TableCell>
)}
{item.totalHours !== undefined && (
<TableCell>{item.totalHours}</TableCell>
)}
{item.totalMoney !== undefined && (
<TableCell>{item.totalMoney}€</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
) : loading ? (
<BarLoader color="#36d7b7" loading={loading} width={"100%"} />
) : (
<h1 className="w-full text-center text-4xl overflow-y-hidden">
Nessun risultato!
</h1>
)}
</div>
);
}
Now the problem:
Let’s say I have 4 Stats components in my page and they all have some kind of result inside. Whenever I try to delete for example the 2nd one, it’ll actually gets deleted and removed from the page (and from the state), but all the subsequent ones will get re-rendered and lose their result “graphically” (their actual result is still available in the results
state). They are still present in the page, but everything that the user put and what was shown is just lost. The 1st instead, will remain intact.
Another example: I have 4 Stats with some result inside, I delete 3rd, 1st and 2nd will remain the same, 3rd gets correctly removed, 4th gets re-rendered and lose their result.
I tried everything. I thought “Maybe some states I use in the Stats component are changing” but no. I debugged every single variable I could debug. I also tried using memo as this post suggests but no luck (if this is indeed the solution, I’d still very much appreciate the correct way to do it in my case).
My only theory is that this problem has something to do with either the index
prop or the key
property every Stats component has. Weirdly, only the subsequent components from the one I deleted get changed, and not the ones before. Maybe I’m doing something wrong with the handleRemoveStats
function? Somehow setting a new array breaks everything?
Another problem I noticed is that when I delete a Stats component, the key
property of said component doesn’t change. This also creates weird cases where if I delete a Stats and then add a new one, potentially two or more Stats will have the same key which may break things (?). The exact thing happens with the prop index
.
The project’s almost done but I can’t get this “delete” functionality to work. I appreciate any help anybody can provide. Thanks!