React:当数组中的一个元素被删除时,所有后续元素都会重新渲染,而前面的元素则不会

问题描述 投票:0回答:1

在这个项目(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

该项目即将完成,但我无法使用此“删除”功能。我感谢任何人可以提供的帮助。谢谢!

reactjs arrays next.js react-hooks server-side-rendering
1个回答
0
投票
  • 不要使用索引作为键(https://react.dev/learn/rendering-lists#rules-of-keys)。 在继续之前请完整阅读链接。
  • 每次添加/删除和项目时创建一个新数组(您已经这样做了,但要确保保留的统计信息与旧数组具有相同的键)

对于键来说,每个统计数据必须始终是唯一的。当您删除一个项目时,例如,它后面的所有项目将获得 key = prevIndex - 1 如果它们保持与之前相同的键,则它们不会重新渲染 - 这就是为什么删除索引之前的项目不会重新渲染

通常该密钥由后端提供(行主键,通常是 UUID),但听起来您的乘客数据是硬编码的,您现在可以使用与每个统计中的乘客 ID 连接的时间戳作为临时措施

另一种不可靠的幼稚方法是删除仅设置

stat.visible = false
并返回
<></>
的方法 - 但这只是目前的一个快速技巧,直到你弄清楚如何获得唯一的 id,我不推荐它用于生产

© www.soinside.com 2019 - 2024. All rights reserved.