在这个项目(React with NextJS)中,用户可以看到各种统计数据,比如某个骑手做了多少订单、工作了多少小时等等。用户不仅限于一个“部分”,他们可以添加和删除任意数量的部分,以便进行比较,例如,某个骑手与另一骑手的统计数据。这里的代码代表显示这些部分的页面。关键是我有一个 useState 包含“Stats”节点数组(我谈到的对话框):
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>
);
}
现在统计组件:
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 使用“StatsResult”,这是实际显示结果的组件(只是一个包含结果的小表)
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>
);
}
现在的问题是:
假设我的页面上有 4 个统计组件,它们内部都有一些结果。每当我尝试删除例如第二个时,它实际上会被删除并从页面(和状态)中删除,但所有后续的都将重新渲染并“以图形方式”丢失其结果(它们的实际结果)在
results
状态下仍然可用)。它们仍然存在于页面上,但用户放置的所有内容和显示的内容都丢失了。相反,第一个将保持不变。
另一个例子:我有 4 个统计数据,里面有一些结果,我删除第 3 个,第 1 个和第 2 个将保持不变,第 3 个被正确删除,第 4 个被重新渲染并丢失其结果。
我什么都试过了。我想“也许我在统计组件中使用的某些状态正在改变”,但事实并非如此。我调试了我可以调试的每个变量。我也尝试使用“备忘录”,如这篇文章建议的那样,但没有运气(如果这确实是解决方案,我仍然非常感谢在我的情况下执行此操作的正确方法)。
我唯一的理论是,这个问题与每个 Stats 组件具有的
index
属性或 key
属性有关。奇怪的是,只有我删除的组件的后续组件发生了变化,而不是之前的组件。也许我对 handleRemoveStats
函数做错了什么?不知怎的,设置一个新数组会破坏一切?
我注意到的另一个问题是,当我删除 Stats 组件时,该组件的
key
属性不会改变。这也会产生奇怪的情况,如果我删除一个统计数据,然后添加一个新的统计数据,则可能两个或多个统计数据将具有相同的密钥,这可能会破坏事物(?)。确切的事情发生在道具上 index
。
该项目即将完成,但我无法使用此“删除”功能。我感谢任何人可以提供的帮助。谢谢!
对于键来说,每个统计数据必须始终是唯一的。当您删除一个项目时,例如,它后面的所有项目将获得 key = prevIndex - 1 如果它们保持与之前相同的键,则它们不会重新渲染 - 这就是为什么删除索引之前的项目不会重新渲染
通常该密钥由后端提供(行主键,通常是 UUID),但听起来您的乘客数据是硬编码的,您现在可以使用与每个统计中的乘客 ID 连接的时间戳作为临时措施
另一种不可靠的幼稚方法是删除仅设置
stat.visible = false
并返回 <></>
的方法 - 但这只是目前的一个快速技巧,直到你弄清楚如何获得唯一的 id,我不推荐它用于生产