0

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.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Jackson
  • 33
  • 1
  • 4

0 Answers0