Cagri Uysal
Cagri Uysal

Reputation: 404

React - can't update state correctly inside single useEffect

I am trying to display some data into the table element using react. The table will have 5 columns and 9 rows.

I am using a state which holds the data to be displayed in the table as array of arrays,

const [displayedSlot, setDisplayedSlot] = useState(
  hours.map(() => days.map(() => ""))
);

where,

const days = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const hours = [
  "8:40-9:40",
  "9:40-10:40",
  "10:40-11:40",
  "11:40-12:40",
  "12:40-13:40",
  "13:40-14:40",
  "14:40-15:40",
  "15:40-16:40",
  "17:40-18:40",
];

I also have two additional states as well,

const [possibleSchedules, setPossibleSchedules] = useState([]); 
const [currentSchedule, setCurrentSchedule] = useState(null);

possibleSchedules holds the data can be displayed in the table(it's filled after some user input), and, currentSchedule holds the index for which possible schedule will be displayed.

When the user changes the value of the currentSchedule, I want to clear the table and display new schedule data in the table.

What I have tried initially as follows,

     useEffect(() => {
        // I was hoping this would clear table before I update it with new data.
        setDisplayedSlot(hours.map(() => days.map(() => ""))); 

        const schedule = possibleSchedules[currentSchedule];

        // this function calls `setDisplayedSlots` to fill the correct data cells,
        // for given `schedule`
        updateTable(schedule);
    }, [currentSchedule]);

This didn't work as I expected, what happens is, the table isn't cleared and data from the previous schedule data remained if the corresponding cell isn't overridden by the newly selected schedule.

I thought this is due to the asynchronous nature of the react state updates. (?)

To work around this, I have added new state,

const [isTableClear, setIsTableClear] = useState(false);

Changed the previous useEffect and added another one,

    useEffect(() => {
        setDisplayedSlot(hours.map(() => days.map(() => ""))); // clears table
        setIsTableClear(true);
    }, [currentSchedule]);

     useEffect(() => {
        if (isTableClear) {
            const schedule = possibleSchedules[currentSchedule];
            updateTable(schedule);
            setIsTableClear(false);
        }
    }, [isTableClear]);

This one works, but I have a feeling, this is a bit of a hack and not a react best practice at all. Since I am fairly new to react, I want to ask if this is a good solution, or how would you solve it?

Upvotes: 2

Views: 65

Answers (1)

Drew Reese
Drew Reese

Reputation: 203587

Any time you are queueing react state updates that depend on previous state then you should use a functional state update.

Issue

You are enqueuing multiple state updates from within the same render cycle, each one using the state from the current render cycle, thus overwriting each previous state update.

Solution

Since you enqueuing more than a single update to displayedSlot you need to do a functional update from the first state update. The first state update in the effect is ok since you are "wiping" state clean, subsequent updates should be functional.

 useEffect(() => {
    setDisplayedSlot(hours.map(() => days.map(() => ""))); // <-- this is ok

    const schedule = possibleSchedules[currentSchedule];

    updateTable(schedule);
}, [currentSchedule]);

updateTable - Use a functional state update to take in any previous enqueued update to update from.

...

setDisplayedSlot(displayedSlot => {
  const updatedDisplayedSlot = displayedSlot.map(
    ...
    // logic to update specific slots
    ...
  );
  ...
  return updatedDisplayedSlot;
});

...

Upvotes: 1

Related Questions