i'm trying to write a hook to be used by two of my components-datepicker and dateRangePicker.
Two components extend antd datepicker and rangerpicker, and have the same way of handling states. DatePicker is for selecting a single date and Rangepicker for range of date.
I am trying to extract common logic out of these components and make a custom hook.
Here's my DateRangePicker. Date Picker Looks almost the same.
import { useRef } from "react";
import { DateRangePickerProps } from "./types";
import * as S from "./styles";
import { ReactComponent as CalenderIcon } from "@assets/images/calender.svg";
import Footer from "./Footer";
import { useDatePicker } from "./hooks";
function DateRangePicker({
isError = false,
withFooter = false,
showNextPage = false,
format = "DD/MM/YYYY",
onChange,
defaultValue,
placeholder = ["From", "To"],
...props
}: DateRangePickerProps) {
const wrapperRef = useRef(null);
const {
confirmedValue,
isOpen,
handleOpen,
handleKeyDown,
handleChange,
handleConfirm,
handleDismiss
} = useDatePicker<DateRangePickerProps>(
defaultValue,
onChange,
withFooter,
wrapperRef
); // calling custom hook
return (
<S.Wrapper ref={wrapperRef}>
<S.RangePicker
{...props}
value={props.value || confirmedValue?.date}
open={isOpen}
onOpenChange={handleOpen}
onKeyDown={handleKeyDown}
onChange={handleChange}
isError={isError}
separator={<S.Separator>-</S.Separator>}
suffixIcon={<CalenderIcon />}
format={format}
getPopupContainer={triggerNode => triggerNode}
withFooter={withFooter}
renderExtraFooter={() =>
withFooter && (
<Footer onConfirm={handleConfirm} onDismiss={handleDismiss} />
)
}
showNextPage={showNextPage}
placeholder={placeholder}
/>
</S.Wrapper>
);
}
export default DateRangePicker;
And here's my Custom hook.
I've used Generic parameter which could either be 'DateRangePickerProps' or 'DatePickerProps'
import { useState, useEffect, useRef, RefObject } from "react";
import { DateRangePickerProps, DatePickerProps } from "./types";
type Event = MouseEvent | TouchEvent;
interface SelectedDate<T extends DateRangePickerProps | DatePickerProps> {
date: T['value'] | null
dateString: T["dateString"] | null;
}
export function useDatePicker<T extends DateRangePickerProps | DatePickerProps>(
defaultValue: T['defaultValue'],
onChange: T['onChange'],
withFooter: boolean,
wrapperRef: RefObject<HTMLElement>
) {
const [isOpen, setIsOpen] = useState<boolean>(false);
// confirmedRange, pendingRange 타입 일치 시키기
const [confirmedValue, setConfirmedValue] = useState<SelectedDate<T>>({
date: defaultValue ?? null,
dateString: null
});
const [pendingValue, setPendingValue] = useState<SelectedDate<T>>({
date: null,
dateString: null
});
const sync = useRef(true);
useEffect(() => {
// invoke user-passed onChange
if (confirmedValue.dateString !== null && confirmedValue && !sync.current) {
onChange?.(confirmedValue.date, confirmedValue.dateString);
sync.current = true;
}
}, [confirmedValue, onChange]);
useEffect(() => {
// prevent previous pendingValue from taking over
setPendingValue({ date: null, dateString: null });
}, [isOpen]);
useOnClickOutside(wrapperRef, () => {
if (withFooter) setIsOpen(false);
});
const handleChange: T['onChange'] = (date, dateString) => {
// I get error here; Parameter 'date' implicitly has an 'any' type.ts(7006)
if (withFooter) setPendingValue({ date, dateString });
else {
sync.current = false;
setConfirmedValue({ date, dateString });
}
};
const handleOpen = (internalOpenState: boolean) => {
if (withFooter && !internalOpenState) return;
setIsOpen(internalOpenState);
};
const handleConfirm = () => {
// prevent null(incomplete date range) from taking over
if (pendingValue) {
setConfirmedValue(pendingValue);
sync.current = false;
}
setIsOpen(false);
};
const handleDismiss = () => {
setIsOpen(false);
};
const handleKeyDown: T["onKeyDown"] = key => {
if (key.code === "Escape") {
setIsOpen(false);
}
};
return {
confirmedValue,
isOpen,
handleOpen,
handleKeyDown,
handleChange,
handleConfirm,
handleDismiss
};
}
DateRangePickerProps and DatePickerProps share common properties but with slight difference. For example, 'onChange' is gets parameters of (moment, string) in datePicker, whereas ([moment, moment], [string, string]) is valid in rangePicker.
I'm using following types
import type { DatePickerProps as AntDatePickerProps } from "antd";
import { RangePickerProps as AntRangePickerProps } from "antd/lib/date-picker";
export interface CustomProps {
isError?: boolean;
withFooter?: boolean;
showNextPage?: boolean;
}
export type DatePickerProps = AntDatePickerProps &
CustomProps & { dateString: string };
export type DateRangePickerProps = AntRangePickerProps &
CustomProps & { dateString: [string, string] };
export interface ButtonProps {
size?: "small" | "medium" | "large";
$type?: "primary" | "secondary";
children?: React.ReactNode;
}
export interface FooterProps {
onDismiss: () => void;
onConfirm: () => void;
}
It seems like my approach of generic interface isn't well understood by typescript. My understanding is that typescript can't figure out which of interface will be passed.
I've left another question regarding this and got an answer, but couldn't implement it. Generic Function and index access
Can anyone come up with better and error-free approach of dealing with this?
Thanks in advance.