0

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

2
  • The rule is that effects should never update your state. They're two different things - effects update part of the UI outside of the standard stateupdate->render mechanism. It's a long read, but go through reactjs.org/docs/hooks-effect.html anyway, especially if you haven't fully read it in its entirely at least once before. Commented May 21, 2021 at 16:10
  • paraphrasing: "Are you saying effects shouldn't set state?" No, I'm not: the React documentation is. Effects are "side effects that happen every time a component updates". If you make them update the state, now they're no longer side effects, they are a functional part of your component's state management, and you shouldn't be using useEffect for whatever functionality you've written up. Again: it's a long page, but read it anyway. It is the most authoritative source, and well written, documenting exactly what useEffect is for. Commented May 21, 2021 at 16:21

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

6 Comments

Understood and you think there's no way around this but to use two separate states
You could create a function instead of a useEffect and call it programatically when you need to update the state, this would prevent a loop like the one in the useEffect, because the useEffect will always execute again when its dependencies update.
whenever you select a date selectDate creates a brand new data and sets it . when you have a dependency in the form of array or object react does shallow comparison where it compares only the reference of the previous array and the current array . since you created a new array . The reference keeps changing which causes the useEffect to run in infinite loop . we should always be passing primitives in the dependency array .
can you clarify what a primitive is?
The primitives he's referencing to are the primitive types in javascript, wich is string, number, boolean, null, undefined, symbol, it does not include arrays and objects and are more recommended for use in useEffects.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.