Skip to content
Advertisement

Prevent infinite loop in useEffect when setting state

I am currently building a scheduling app. If a user selects two dates, I am attempting to select all date blocks between the two selected dates in the calendar as well. I am able to achieve this, but it causes my useEffect to fire into an infinite loop because I have state as a dependency in my useEffect where I am setting state. I am unsure of the best method to prevent the infinite loop behavior. The useEffect in question is the bottom one. My code is as follows:

export default function App() {
  const [selectedDate, handleDateChange] = useState(
    dayjs().format("YYYY-MM-DD")
  );
  const [events] = useState([
    {
      id: "5e24d1fa-aa66-4122-b1eb-97792f0893b0",
      name: "Rodriquez Family",
      selectedDates: ["2021-05-01"],
      status: "submitted"
    },
    {
      id: "269a0381-63c7-4ab6-92d8-7f7b836aee6f",
      name: "Test Family",
      selectedDates: ["2021-05-03"],
      status: "submitted"
    }
  ]);
  const [data, setData] = useState([]);

  const getDaysArray = async (firstDay, lastDay) => {
    let dates = [];
    var dow = dayjs(firstDay).day();
    while (dow > 0) {
      dates.push(null);
      dow = dow - 1;
    }

    while (firstDay <= lastDay) {
      dates.push(firstDay);
      firstDay = dayjs(firstDay).add(1, "days").format("YYYY-MM-DD");
    }

    return dates;
  };

  useEffect(() => {
    const getDates = async () => {
      const firstDay = dayjs(selectedDate)
        .startOf("month")
        .format("YYYY-MM-DD");
      const lastDay = dayjs(firstDay).endOf("month").format("YYYY-MM-DD");
      const dates = await getDaysArray(firstDay, lastDay);

      const list = dates.map((date) => {
        const event = events.find(({ selectedDates = [] }) =>
          selectedDates.includes(date)
        );
        return event ? { date, event } : { date, event: null, checked: false };
      });

      setData(list);
    };
    getDates();
  }, [events, selectedDate]);

  const selectDate = (date) => {
    setData(
      (a) =>
        a &&
        a.map((item) =>
          item.date === date ? { ...item, checked: !item.checked } : item
        )
    );
  };

  useEffect(() => {
    if (data && data.filter((res) => res.checked).length > 1) {
      const filterDates = data.filter((r) => r.checked);
      const startDate = filterDates[0].date;
      const endDate = filterDates[filterDates.length - 1].date;

      const datesToUpdate = data.filter(
        (res) => res.date > startDate && res.date < endDate
      );

      const newArr = data.map((date) => {
        const updateCheck = datesToUpdate.find((r) => r.date === date.date);

        return updateCheck ? { ...updateCheck, checked: true } : date;
      });

      setData(newArr);
    }
  }, [data]);

  return (
    <MuiPickersUtilsProvider utils={DayJsUtils}>
      <div className="App">
        <DatePicker
          minDate={dayjs()}
          variant="inline"
          openTo="year"
          views={["year", "month"]}
          label="Year and Month"
          helperText="Start from year selection"
          value={selectedDate}
          onChange={handleDateChange}
        />
      </div>
      <div className="cal">
        <div className="cal-div1"></div>
        <div className="cal-div2 "></div>
        <div className="cal-div3 cal-cir-hov"></div>
        <div className="cal-div4"> SUN </div>
        <div className="cal-div5"> MON </div>
        <div className="cal-div6"> TUE </div>
        <div className="cal-div7"> WED </div>
        <div className="cal-div8"> THU </div>
        <div className="cal-div9"> FRI </div>
        <div className="cal-div10"> SAT </div>
        {data &&
          data.map((r, i) => {
            return (
              <>
                <div
                  onClick={() =>
                    !r.checked &&
                    r.date >= dayjs().format("YYYY-MM-DD") &&
                    !r.event &&
                    selectDate(r.date)
                  }
                  style={
                    r.checked
                      ? { backgroundColor: "green" }
                      : { color: "#565254" }
                  }
                  key={i}
                  className="cal-cir-hov"
                >
                  <div>{r.date} </div>
                  <div
                    style={
                      r.event?.status === "submitted"
                        ? { color: "orange" }
                        : { color: "green" }
                    }
                  >
                    {r.event?.name}
                  </div>
                </div>
              </>
            );
          })}
      </div>
    </MuiPickersUtilsProvider>
  );
}

attached is a code sandbox for debugging and to show the behavior I am currently talking about. Select two separate dates that are greater than today and you will see all the dates in between are selected, but the app goes into a loop https://codesandbox.io/s/dawn-snow-03r59?file=/src/App.js:301-4499

Advertisement

Answer

If your useEffect depends on a variable that you’re updating on the same useEffect there will always be the re-render and cause a loop.

If you want it to execute only once, you should remove the data variable from the useEffect dependency array.

But if you really wanna mutate the state every time that the data variable changes, my recommendation is to create another state for the mutated data.

For example setFormattedData would not change the data itself, but you would still have a state for this data in the format that you want.

Advertisement