gpbl/react-day-picker

Range mode has its own internal state and does not respect the controlled state

Closed this issue ยท 4 comments

Code

import { useState } from 'react';

import { DateRange, DayPicker } from 'react-day-picker';

export function ControlledCalendar() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  return (
    <DayPicker
      mode="range"
      selected={selected}
      onSelect={(selected) => {
        // setSelected(selected);
      }}
    />
  );
}

Expected Behavior

Since the component is being used in "controlled" mode, it should use the state passed via "selected" prop. In the example above, the selected state is purposely not updated. It was expected that the calendar would not work, since the selected values aren't being updated.

I was trying to implement a behavior where if the range is already selected, and the user click on a new day, the range is reset:

import { useState } from 'react';

import { DateRange, DayPicker } from 'react-day-picker';

export function ControlledCalendar() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  function handleOnSelect(range: DateRange, triggerDate: Date) {
    if (selected?.from && selected?.to) {
      setSelected({
        from: triggerDate,
        to: undefined,
      });
      return;
    }
    setSelected(selected);
  }

  return <DayPicker mode="range" required selected={selected} onSelect={handleOnSelect} />;
}

Since the internal state is updated via useEffect, it causes a small flicker. The sequence of events is:
User click -> internal range state updated -> onSelect callback -> external state updated (range reset) -> internal state is updated via useEffect due external state change.

Actual Behavior

The calendar has its own internal state. The external state is updated via useEffect.

CodeSandbox

Video

Tab-App.tsx.nodebox.CodeSandbox.mp4

PS: I throttled the CPU in 6x to make it more noticeable

gpbl commented

thanks @matheusmb for your detailed report ! you are right the controlled vs uncontrolled state isn't the ideal here.

The code should read something like:

const handleSelect = (value: string) => {
    if (onSelect) {
      onSelect(value); // If the component is controlled, trigger the onSelect callback
    } else {
      setInternalSelected(value); // If the component is uncontrolled, update the internal state
    }
  };
gpbl commented

Hey this is more difficult than I thought. Please have patience...

gpbl commented

@matheusmb to help - could you create a (failing) test case, reproducing the issue? ๐Ÿ™๐Ÿฝ