React: when an element in an array gets deleted, all subsequent elements gets re-rendered, while the previous don’t

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!

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật