-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useTimescape
/ useTimescapeRange
should be Reactive by default
#40
Comments
useTimescape
should be Reactive by defaultuseTimescape
/ useTimescapeRange
should be Reactive by default
Hey @junwen-k, thanks for flagging this! I agree that this would be a good change for the React implementation. However, I am not so sure about the other integrations (because I haven't worked intensively with them before). I don't know if I'll be able to get to this soon, but if you want to shoot a PR I'd appreciate it. No worries if not :) |
Here is a user land version of import React from "react";
// eslint-disable-next-line import-x/no-named-as-default
import TimescapeManager from "timescape";
import type { DateType, Options } from "timescape/react";
export type TimepickerOptions = Omit<Options, "date" | "onChangeDate"> & {
defaultDate?: Date | null;
date?: Date | null;
onChangeDate?: (nextDate: Date | null) => void;
};
// rewritten from https://github.com/dan-lee/timescape/blob/main/packages/lib/src/integrations/react.ts
// to support reactivity
// see https://github.com/dan-lee/timescape/issues/40
export const useTimepicker = (options: TimepickerOptions = {}) => {
const { date, onChangeDate, defaultDate, ...rest } = options;
const [manager] = React.useState(
() => new TimescapeManager(date ?? defaultDate ?? undefined, rest)
);
const onChangeDateRef = React.useRef(onChangeDate);
const isUpdatingFromProps = React.useRef(false);
React.useLayoutEffect(() => {
onChangeDateRef.current = onChangeDate;
}, [onChangeDate]);
React.useEffect(() => {
// this is to work around StrictMode shenanigans
manager.resync();
return () => {
manager.remove();
};
}, [manager]);
React.useEffect(() => {
return manager.on("changeDate", (nextDate) => {
// only call onChangeDate if the change came from the input/manager
// do not call onChangeDate if the change came from the date prop
if (
!isUpdatingFromProps.current &&
nextDate?.getTime() !== date?.getTime()
) {
onChangeDateRef.current?.(nextDate ?? null);
}
});
}, [date, manager]);
const dateTimestamp = date?.getTime();
const managerDateTimestamp = manager.date?.getTime();
const isDateUndefined = date === undefined;
React.useEffect(() => {
if (dateTimestamp !== managerDateTimestamp && !isDateUndefined) {
// disable onChangeDate while updating from props
isUpdatingFromProps.current = true;
manager.date = dateTimestamp ?? undefined;
isUpdatingFromProps.current = false;
}
}, [dateTimestamp, isDateUndefined, manager, managerDateTimestamp]);
React.useEffect(() => {
if (!isDateSame(rest.minDate, manager.minDate))
manager.minDate = rest.minDate;
if (!isDateSame(rest.maxDate, manager.maxDate))
manager.maxDate = rest.maxDate;
if (rest.hour12 !== manager.hour12) manager.hour12 = rest.hour12;
if (rest.digits !== manager.digits) manager.digits = rest.digits;
if (rest.wrapAround !== manager.wrapAround)
manager.wrapAround = rest.wrapAround;
if (rest.snapToStep !== manager.snapToStep)
manager.snapToStep = rest.snapToStep;
if (rest.wheelControl !== manager.wheelControl)
manager.wheelControl = rest.wheelControl;
if (rest.disallowPartial !== manager.disallowPartial)
manager.disallowPartial = rest.disallowPartial;
}, [
manager,
rest.digits,
rest.disallowPartial,
rest.hour12,
rest.maxDate,
rest.minDate,
rest.snapToStep,
rest.wheelControl,
rest.wrapAround,
]);
// non-prod warnings
if (!import.meta.env.PROD) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const wasControlled = React.useRef<boolean>(date !== undefined);
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
// Check for simultaneous defaultDate and date props
if (defaultDate != null && date !== undefined) {
// eslint-disable-next-line no-console
console.warn(
"Warning: TimePicker component received both `date` and `defaultDate` props. " +
"`date` will take precedence. Remove `defaultDate` when using TimePicker as a controlled component."
);
}
const isControlled = date !== undefined;
const wasControlledValue = wasControlled.current;
if (isControlled !== wasControlledValue) {
const mode = isControlled ? "controlled" : "uncontrolled";
// eslint-disable-next-line no-console
console.warn(
`Warning: TimePicker component is changing from ${
wasControlledValue ? "controlled" : "uncontrolled"
} to ${mode}. ` +
`This may lead to unexpected behavior. Decide between controlled or uncontrolled for the entire lifecycle.`
);
}
wasControlled.current = isControlled;
}, [date, defaultDate]);
}
return {
_manager: manager,
getInputProps: (
type: DateType,
opts?: {
ref?: React.MutableRefObject<HTMLInputElement | null>;
autofocus?: boolean;
}
) => ({
ref: (element: HTMLInputElement | null) => {
if (!element) return;
manager.registerElement(element, type, opts?.autofocus);
if (opts?.ref) opts.ref.current = element;
},
}),
getRootProps: () => ({
ref: (element: HTMLElement | null) =>
element && manager.registerRoot(element),
}),
} as const;
};
// helper for comparing min/max dates
function isDateSame(
a: Date | "$NOW" | undefined,
b: Date | "$NOW" | undefined
) {
if (a instanceof Date && b instanceof Date)
return a.getTime() === b.getTime();
return a === b;
} |
Thanks for that! I haven't got around to it yet. PR would be greatly appreciated |
yea understood - I'm reimplementing the range with reactivity now that STOP_EVENT_PROPAGATION is exposed then I'll try to PR my changes back along with tests. |
Recently I've just came across this library, exactly what I was looking for a long time, awesome! However, there is a slight problem with the React (and possibly other frameworks) integration.
Problem
Currently,
useTimescape
is not declarative by default, and to sync the state with external sources, we need to useuseEffect
to handle it manually.Suggestion
To better align with the declarative nature of React,
useTimescape
should be reactive by default, without requiring users to callupdate()
imperatively. For instance:This change aligns well with the React ecosystem API, making it more predictable and declarative.
Remark
Some popular libraries that uses the method that I described
This proposed change could also potentially apply to other frameworks like Solid.
Thank you so much for this awesome library.
The text was updated successfully, but these errors were encountered: