The `selected` prop is not working without setting `onSelect`
To reproduce
stackblitz Link: https://stackblitz.com/edit/vitejs-vite-rdyvfyjg?file=src%2FApp.tsx
import { useState } from 'react';
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/style.css';
const DAY_IN_MS = 24 * 60 * 60 * 1000;
function App() {
const [range, setRange] = useState({
from: new Date(),
to: new Date(),
});
const changerPrev = () => {
setRange({
from: new Date(range.from.getTime() - DAY_IN_MS),
to: range.to,
});
};
return (
<>
<button onClick={() => changerPrev()}>prev</button>
{/* it doesn't work */}
<DayPicker mode="range" selected={range} />
{/* it work */}
<DayPicker mode="range" selected={range} onSelect={() => void 0} />
</>
);
}
export default App;
Actual Behavior
In the first case, the selected value does not change. In the second case, the selected value changes.
Expected Behavior
In both the first and second cases, the selected value should change.
Screenshots
Hi @jiwon79 Thanks for the feedback!
@gpbl
I agree with @jiwon79, I noticed this behavior and was planning to discuss at some point.
Internally the DayPicker only uses the selected prop if the onSelect is defined.
IMO, it's not ideal because developers may want to use other ways to control the selected date.
This GH issue has one example, and another one that comes to my mind is using the onDayClick event handler instead of onSelect to compute the selected value.
What do you think of removing this behavior and only using the selected prop if it's defined?
The issue with this change of behavior is that I think it's a breaking change, isn't it?
Yes, it’s a BC, but I think we have to removing this behavior.
In all input-like components, component is uncontrolled when value is undefined. The current DayPicker range mode's behavior is hard to predict for someone using it for the first time.
Also bumped into this issue. I'm migrating from v7 to v9. Couldn't figure out why the selected prop wasn't working as before.
Thanks for the feedback @devdpontes.
I think the original idea was a similar behavior as an HTML checkbox in React, where the checked and onChange props are designed to work together. Wondering if a defaultSelected could help here to reintroduce the same behavior - without adding a breaking change. Thoughts?
@gpbl
I tested the idea of the dafaultSelected, there are some caveats about it, even about the current API, lemme share my thoughts.
-
I think
dafaultSelectedcould indeed be a workaround for this issue, but I think it's technically a breaking change to change the behavior from theselectedprop only work whenonSelectis defined to always using theselectedvalue. -
The
reacty-day-pickerright now usesundefinedto represent an empty value (no date selected), which works fine most of the time, but there's an edge case that would cause a bug: when trying to reset theselectedvalue from outside theDayPickertoundefined, internally theDayPickerwill switch touncontrolledmode because using onlyundefineddoesn't allow the logic to differentiate between an empty value or the prop not being passed to work asuncontrolledmode.
E.g. if theDayPickeris used in a form, and there's "reset form" button that resets the form state to the initial values, if theselectedprop changes toundefined, theDayPickerwould not consider this as an empty value, but asselectednot being passed, thus switching touncontrolledmode, and using the internal state of the component.
This is why theSpectrum DatePickerAPI for theselectedanddefaultSelectedacceptsDateValue | null,nullrepresenting the empty value for the date picker.
Changing the API to acceptnullwould be significant breaking change though, requiring further discussion. -
I'd like to also discuss the relevance of having an
uncontrolledmode with perhaps adefaultValueprop.
For input component like theDayPicker, the majority of the use cases, developers are interested in what is theselecteddate state, to use it somewhere else in the UI or send it to the server in a form submission.
UsingSpectrum DatePickeragain as reference, it acceptsdefaultValueforuncontrolledmode, but besides the prop, the SpectrumDatePickermounts a hiddeninputwith the internalselected/valuestate, which allows theuncrontolledstate to be sent as part of a form submission.
Thereacty-day-pickerdoesn't mount thisinputthough, which means that developers need to a always keep track of theselectedvalue to then use it. (developers may not pass theselecteddate state to theDayPickerasselectedprop, but the state most of the use cases needs to be managed outside the component to be able to use it elsewhere)
What do you think of removing theuncontrolledmode?
This would simplify the API, and also prevent developers from running into the bug I mentioned in point 2, where the state changes outside theDayPickerbut it doesn't reflect to the component itself because it's inuncontrolledmode.
@rodgobbi, thanks so much for your thoughtful response!
I see how the behavior of the selected and onSelect props might be confusing for users. Our main goal should be to avoid breaking changes while focusing on progressive enhancements. Since many users are still on v8 or v7, we should only consider breaking changes once v9 has broader adoption.
Few ideas:
- To make the migration to v9 easier, we could start by adding a note to the upgrading guide.
- If I understand correctly, introducing a new
defaultSelectedprop might simplify the upgrade process to v9 (e.g., "replaceselectedwithdefaultSelected") without requiring a major release.- Another idea could be to make the
modifiers={{ selected: selectedDates }}prop work withoutonSelect. However, I’m not sure how much of a breaking change this would cause.
- Another idea could be to make the
- While I agree the uncontrolled mode should be removed, it would also be a breaking change. It might be better to save this for a future major release.
- I like the idea of using
nullas a workaround for issues withundefined. Do you think this could be implemented in a minor release?
I think
defaultSelectedcould indeed be a workaround for this issue, but I think it's technically a breaking change to change the behavior from theselectedprop only working whenonSelectis defined to always using theselectedvalue.
I’m not sure I fully understand your point here. The idea behind defaultSelected is to avoid changing the behavior of the selected and onSelect props. Could you clarify?
@gpbl for sure, thanks for the feedback 👍
While I agree the uncontrolled mode should be removed
Thanks for providing this feedback. I wanted to confirm if it also makes sense for you.
we should only consider breaking changes once v9 has broader adoption
Good point. I wasn't sure how it would be the best time to work on breaking changes, but you gave a good guideline. 👍
I like the idea of using null as a workaround for issues with undefined. Do you think this could be implemented in a minor release?
I think not, because existing consumers that rely on undefined as a selected value meaning the date is empty would break. E.g. onSelect is called with undefined when unselecting the date, which would break any code doing equality check like === undefined.
To be able to start supporting null, we would need to separate the old API referring to undefined and only use null for the new API.
It seems to me it's not safe to infer this, so the only approach that comes to my head now is having an explicit API like nullEmptyValue, which would treat null as the empty value.
I don't know if this is worth it. This could be used as a migration plan for a future breaking change.
Let me know if we should think more about it and try to come up with other approaches.
I’m not sure I fully understand your point here. The idea behind defaultSelected is to avoid changing the behavior of the selected and onSelect props. Could you clarify?
I see your point.
I understand a bit better now, so the goal of the defaultSelected you proposed is to have a more explicit API that is equal to selected without onSelect defined. This would not be a breaking change for sure.
But this implementation of defaultSelected doesn't solve this GH issue, as the suggestion here is to always use the selected prop as the selected date, which right now only is used when onSelect is defined.
I think the suggestion to "replace selected with defaultSelected" in the migration guide will not work for all use cases, as the intention of the prop is to use the component in controlled mode, so I feel that writing something like "always pass a onSelect callback to use in controlled mode" would cover the migration of this use case.
And we could also add defaultSelected for consumers that want to use it in uncontrolled mode.
What do you think?