react hook with generic interface - reactjs

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.

Related

TypeScript Polymorphic Components in React with conditional props

I'm trying to write a Polymorphic component in React that supports both host elements such as (div, button, etc) and custom React components.
but there's a constraint of needing whatever component is passed in to the as prop that it must have an onClick prop
here's my work in progress.
import React, { useCallback, useRef, useLayoutEffect } from 'react';
export type GaClickTrackerProps<Type extends React.ElementType> = {
as: Type;
fieldsObject: {
hitType: UniversalAnalytics.HitType; // 'event'
eventCategory: string;
eventAction: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
nonInteraction?: boolean | undefined;
};
onClick?: (...args: any[]) => any;
} & React.ComponentPropsWithoutRef<Type>;
export function GaClickTracker<Type extends React.ElementType>({
onClick,
fieldsObject,
as: Component,
...props
}: GaClickTrackerProps<Type>) {
const fieldsObjectRef = useRef(fieldsObject);
const onClickRef = useRef(onClick);
useLayoutEffect(() => {
onClickRef.current = onClick;
fieldsObjectRef.current = fieldsObject;
});
const handleClick: React.MouseEventHandler<Type> = useCallback((event) => {
onClickRef.current?.(event);
if (fieldsObjectRef.current) {
ga('send', fieldsObjectRef.current);
}
}, []);
return (
<Component onClick={handleClick} {...props} />
);
}
Instead of going into the "polymorphic component" direction, for this particular issue it would be far better to have an util function like this:
import type { MouseEventHandler } from "react";
type Fields = {
eventAction: string;
eventCategory: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
hitType: UniversalAnalytics.HitType;
nonInteraction?: boolean | undefined;
};
export const sendAnalytics =
(fields?: Fields) =>
<Type>(handler?: MouseEventHandler<Type>): MouseEventHandler<Type> =>
event => {
handler?.(event); // We call the passed handler first
// And we only call `ga` if the we have fields and the default wasn't prevented
return !event.defaultPrevented && fields
? ga("submit", fields)
: undefined;
};
And then you can use it like this:
import { sendAnalytics } from "~/utils/sendAnalytics.js";
const sendIDKAnalytics = sendAnalytics({
eventAction: "IDK",
eventCategory: "IDK",
hitType: "IDK",
});
export const TrackedButton = ({ onClick, ...props }) => (
<button onClick={sendIDKAnalytics(onClick)} {...props} />
);
Being a curried function, you could even reuse the same settings in different components if you wanted. And if you want to "Reactify" it further, you could make it a hook, like useAnalytics and go from there.
The "polymorphic" approach would make it unnecessarily complex with no real gains to DX:
import { createElement } from "react";
// This:
const Polymorphic = ({ as: Component, ...props }) => <Component {...props} />;
// Is not different from just doing this:
const Polymorphic = ({ as, ...props }) => createElement(as, props);
// And when using any of those, your code would look something like this,
// which is not ideal:
<Polymorphic as="main">
<Polymorphic as="nav">
<Polymorphic as="a" href="https://example.com">
Hello
</Polymorphic>
</Polymorphic>
</Polymorphic>;

How to add static props type for ForwardRefExoticComponent

I have a Modal FC, I need to add two static methods show and hide to it. I have no idea how to add the ModalStaticProps type for the component. So that I can assign show and hide to Modal directly without using type assertion.
import React, { ReactNode, RefAttributes } from 'react';
interface ModalProps {
title: ReactNode;
}
interface ModalStaticProps {
show(): void;
hide(): void;
}
const Modal: React.ForwardRefExoticComponent<ModalProps & RefAttributes<HTMLDivElement>> = React.forwardRef<
HTMLDivElement,
ModalProps
>(({ title }: ModalProps, ref) => {
return <div ref={ref}>{title}</div>;
});
// I want to this after add `ModalStaticProps` type
// Modal.show = () => { };
// Modal.hide = () => { };
// Not this
const ModalComponent = Modal as React.ForwardRefExoticComponent<ModalProps & RefAttributes < HTMLDivElement >> & ModalStaticProps
ModalComponent.show = () => { };
ModalComponent.hide = () => { };
function Consumer() {
return <div onClick={() => ModalComponent.show()} />
}
TypeScript Playground
It is doable.
See Properties declarations on functions. It is possible to do, in a natural way, but it might break your ref types.
Hence, I decided just use Object.assign
See example:
import React, { ReactNode, RefAttributes, ForwardRefExoticComponent } from 'react';
interface ModalProps {
title: ReactNode;
}
interface ModalStaticProps {
show(): void;
hide(): void;
}
const STATIC_PROPS: ModalStaticProps = {
show: () => { },
hide: () => { }
}
const withStaticProps = <T,>(
forwarded: ForwardRefExoticComponent<ModalProps & RefAttributes<HTMLDivElement>>,
staticProps: T
) => Object.assign(forwarded, staticProps)
const Modal = React.forwardRef<
HTMLDivElement,
ModalProps
>(({ title }: ModalProps, ref) => <div ref={ref}>{title}</div>)
const ModalComponent = withStaticProps(Modal, STATIC_PROPS)
function Consumer() {
return <div onClick={() => ModalComponent.show()} />
}
Playground
See my question regarding this problem

Function and data not appearing on type ContextProps. React Context with TypeScript

I'm getting some vague errors when using React Context with TypeScript to pass things down.
The app works well and displays everything it needs to and the functions work yet TypeScript is yelling at me.
Heres my Context being created.
import React, { createContext, useContext, useEffect, useState } from 'react';
type PokemonProps = {
number: string;
name: string;
image: string;
};
type ContextProps = {
capturedPokemons: PokemonProps[];
catchPokemon: (newPokemon: PokemonProps[]) => void;
releasePokemon: (id: string) => void;
};
const CaughtContext = createContext<ContextProps | null>(null);
export const useCaught = () => useContext(CaughtContext);
export const CaughtProvider: React.FC<ContextProps> = ({ children }) => {
const [capturedPokemons, setCapturedPokemons] = useState<any>([]);
const catchPokemon = (newPokemon: PokemonProps[]) => {
if (capturedPokemons.length >= 6) {
alert('You cannot carry any more Pokemon.');
return;
}
const alreadyCaptured = capturedPokemons.some(
(p: PokemonProps) => p.name === newPokemon[0].name
);
if (alreadyCaptured) {
alert('you already have that pokemon');
return;
}
if (window.confirm('Capture Pokemon')) {
setCapturedPokemons([...capturedPokemons, ...newPokemon]);
}
};
return (
<CaughtContext.Provider
value={{ catchPokemon, capturedPokemons, releasePokemon }}>
{children}
</CaughtContext.Provider>
);
};
When using the above code to use the functions or state in my app for example const { catchPokemon } = useCaught(); I get and error "Property 'catchPokemon' does not exist on type 'ContextProps | null'." How does catchPokemon not exist on type ContextProps when I have created the type here:
type ContextProps = {
capturedPokemons: PokemonProps[];
catchPokemon: (newPokemon: PokemonProps[]) => void;
releasePokemon: (id: string) => void;
};
Once again everything works as expected but Typescript is telling me something is wrong with vague solutions.

Using Context API in TypeScript

I'm creating a context using Typescript for the first time, and I'm having a hard time making it work. Everytime I try to create interfaces and put them in my value prop I get errors, and I really need some help. Here's my context(I'll point the errors in comments)
In the IContext, I don't know how can I put the type of the transactions(it is an array of objects)
I'd like to pass everything in the value prop, the two functions, the transactions array, and the inputs.
anyway, that's my first time applying typescript in a bigger project, so if you guys have any advices on how to practice it better, just let me know.
import React, { createContext, useState, ChangeEvent } from 'react';
interface IContext {
handleInputChange(): void;
handleSubmit(): void;
inputElements: {
name: string;
amount: string;
};
transactions: <-- I don't know what to put here
}
export const TransactionsContext = createContext<IContext | null>(null)
interface ITransaction {
name: string;
amount: string;
}
interface ITransactions {
transactionList: ITransaction
}
export const TransactionsContextProvider: React.FC = ({ children }) => {
const [transactions, setTransactions] = useState<ITransactions[]>([])
const [inputs, setInputs] = useState<ITransaction>({
name: '',
amount: ''
})
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setInputs({...inputs, [name]: value })
}
const handleSubmit = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setTransactions([...transactions, inputs]) <-- Error here
}
return (
<TransactionsContext.Provider
value={{ transactions, handleInputChange, handleSubmit, inputs }}> <-- Error here
{children}
</TransactionsContext.Provider>
)
}
Error
In the setTransactions:
Argument of type '(ITransaction | ITransactions)[]' is not assignable to parameter of type 'SetStateAction<ITransactions[]>'.
Type '(ITransaction | ITransactions)[]' is not assignable to type 'ITransactions[]'.
Type 'ITransaction | ITransactions' is not assignable to type 'ITransactions'.
Property 'transactionList' is missing in type 'ITransaction' but required in type 'ITransactions'.
You don't need interface ITransactions.
use ITransaction[] as type for transactions
const [transactions, setTransactions] = useState<ITransaction[]>([])
import React, { createContext, useState, ChangeEvent } from 'react';
interface ITransaction {
name: string;
amount: string;
}
interface IContext {
handleInputChange(e: ChangeEvent<HTMLInputElement>): void;
handleSubmit(e: ChangeEvent<HTMLInputElement>): void;
inputElements: {
name: string;
amount: string;
};
transactions: ITransaction[];
}
export const TransactionsContext = createContext<IContext | null>(null)
export const TransactionsContextProvider: React.FC = ({ children }) => {
const [transactions, setTransactions] = useState<ITransaction[]>([])
const [inputs, setInputs] = useState<ITransaction>({
name: '',
amount: ''
})
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setInputs({...inputs, [name]: value })
}
const handleSubmit = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setTransactions([...transactions, inputs]);
}
return (
<TransactionsContext.Provider
value={{ transactions, handleInputChange, handleSubmit, inputs }}>
{children}
</TransactionsContext.Provider>
)
}

React Typescript - Type error between passing props to Hooks

I have created this codesandbox replicating my issue
1) I first created the <Input> component, (for styling and track if the input has content or not.
2) Everything works, but as need to add more forms to the project, i thought, damn maybe i could also created an useInput hook to just manage the value update instead of having to add onChange: {e => {setSomething(e.target.value)}} all the time.
So i created these useInput, but i got this annoying red linter errors. Its probably some basic type issue, but i can figure it out how to get rid of this issue. without any type solutions
? Thanks in advance
Error screenshot and chunk of code below, but better test in the sandbox
# useInput.tsx
import { useState, ChangeEvent } from "react";
export type onChangeType = (event: ChangeEvent<HTMLInputElement>) => void;
const useInput = (initialValue = "") => {
const [value, setValue] = useState(initialValue);
const reset = () => setValue("");
const onChange: onChangeType = e => {
setValue(e.target.value);
};
return [value, onChange, reset];
};
export default useInput;
# Input.tsx
import React, { useState, ChangeEvent } from "react";
import styled, { css } from "styled-components";
import onChangeType from "./hooks/useInput";
interface iLabelProps {
hasContent: boolean;
}
const hasContentCSS = () => css`
border: 5px solid royalblue;
`;
const Label = styled.label<iLabelProps>```
interface iInput {
readonly type?: string;
readonly name: string;
readonly label: string;
value?: string | number | string[] | null;
defaultValue?: string | number | string[] | null;
readonly onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}
export const Input = ({
name = "email",
label,
value = null,
defaultValue = null,
onChange = null
}: iInput) => {
const [hasContent, setHasContent] = useState(!!defaultValue);
const onBlur = value => {
setHasContent(value.length > 0);
};
return (
<Label hasContent={hasContent}>
<input
type="text"
name={name}
{...defaultValue && { defaultValue: defaultValue }}
{...!defaultValue && { value: value ? value : "" }}
{...onChange && { onChange: onChange }}
onBlur={e => onBlur(e.target.value)}
/>
<span>{label}</span>
</Label>
);
};
The problem came from the incorrectly inferred type of the returned value from the useInput hook. TS think that the type is (string | onChangeType)[]. That means that string or onChangeType can be at any position in the array, while you have very fixed order.
To fix this problem you have to help it a little bit and either cast the array you return like this
return [value, onChange, reset] as [string, onChangeType, () => void];
or specify explicitly the return type of the useInput function
const useInput = (initialValue = ""): [string, onChangeType, () => void] => {...}

Resources