이전 포스트 에서 만들었던 FormInput 컴포넌트의 두가지 문제점을 를 제네릭을 이용해서 개선해보도록 하겠습니다.
[개선된 예제]
- types/props.d.ts
import { ControllerRenderProps, FieldValues, UseControllerProps } from 'react-hook-form'
type WithoutFormPropsComponent<ComponentProps = unknown> = Omit<ComponentProps, keyof ControllerRenderProps>
export type FormComponentProps<
Field extends FieldValues,
ComponentProps = unknown,
> = WithoutFormPropsComponent<ComponentProps> & UseControllerProps<Field>
여러가지 컴포넌트를 form 컴포넌트로 만드는 경우 중복되는 타입 선언을 없애기 위해 FormComponentProps 타입을 만들어줍니다.
- WithoutFormPropsComponent : 기존 컴포넌트의 Props에서 ControllerRenderProps 와 겹치는 props key를 제외시킨 타입입니다.
- FormComponentProps : 제네릭의 첫번째 인자(Field)는 FieldValues의 확장타입, 두번째 인자(ComponentProps)는 사용할 컴포넌트 props의 타입을 주입받아 UseControllerProps<Field> 와 WithoutFormPropsComponent<ComponentProps>를 인터섹션을 한 타입입니다.
복잡해 보일수 있지만 ComponentProps에 TextFieldProps를 주입하면 이전 포스트 의 FormInputProps 타입에서 Field 제네릭을 추가한것과 같은 의미입니다.
- FormInput/FormInput.d.ts
import { TextFieldProps } from '@mui/material'
import { FormComponentProps } from 'types/props'
import { FieldValues } from 'react-hook-form'
export type Validator = (v: string) => boolean
export interface FormInputProps<T extends FieldValues> extends FormComponentProps<T, TextFieldProps> {
validator?: Validator
}
export type OnChangeWithValidator = (validator?: Validator) => TextFieldProps['onChange']
- Validator : react-hook-form 에서 유효성 검사 기능을 제공하지만 추가적인 유효성 검사로직이 필요한 경우 사용하기 위한 validator 함수 타입입니다.
- FormInputProps : 위에서 선언한 FormComponentProps의 두번째 제네릭 인자(ComponentProps)에 TextFieldProps 를 추가하고, Field 에 대한 제네릭은 외부에서 주입받는 타입입니다.
- OnChangeWithValidator : FormInput의 onChange 이벤트 핸들러 타입, validator를 인자로 받아 원래 TextFieldProps의 onChange 핸들러를 반환합니다.
- FormInput/index.tsx
import { TextField } from '@mui/material'
import { FieldValues, useController } from 'react-hook-form'
import { FormInputProps, OnChangeWithValidator } from './FormInput'
export default function FormInput<T extends FieldValues>({
name,
rules,
control,
shouldUnregister,
defaultValue,
validator,
...textFieldProps
}: FormInputProps<T>) {
const { field } = useController({
name,
rules,
control,
shouldUnregister,
defaultValue,
})
const { onChange, ...restField } = field
const handleChange: OnChangeWithValidator = validator => e => {
if (!validator || validator(e.target.value)) {
onChange(e)
}
}
return <TextField onChange={handleChange(validator)} {...restField} {...textFieldProps} />
}
이전 포스트 의 FormInput 컴포넌트에서 Field 에 대한 타입을 이 컴포넌트를 사용하는 컴포넌트에서 주입하도록 개선하고, onChange 이벤트에서 유효성검사로직을 추가한 컴포넌트입니다.
자 이제 이 컴포넌트를 사용해보겠습니다.
- App.tsx
import { Box, Button } from '@mui/material'
import FormInput from 'components/FormInput'
import React from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { container, layout } from 'styles'
import { timeValidator } from 'utils'
const defaultValues = {
FormInput: 12,
}
type FieldType = typeof defaultValues
export default function App() {
const { control, handleSubmit } = useForm({
reValidateMode: 'onBlur',
defaultValues,
})
const handleOnSubmit: SubmitHandler<FieldType> = data => {
alert(data.FormInput)
}
return (
<Box sx={layout} component="form" onSubmit={handleSubmit(handleOnSubmit)}>
<Box sx={container}>
<FormInput control={control} name="FormInput" label={'Input'} />
<Button type="submit">Submit</Button>
</Box>
</Box>
)
}
사용법은 이전 포스트 와 다른점이 거의 없습니다.
useForm 의 제네릭인자를 any로 지정하지 않아도 control에서 타입에러를 발생시키지 않습니다.
name 프로퍼티에 전체 string 타입이 아닌 정확히 FieldType의 key 만 지정할 수 있습니다.
또 onSumbit 핸들러의 data 인자에서 FieldType에 지정한 키로 접근할 수 있습니다.
FormInput 컴포넌트가 제네릭을 가진 컴포넌트이고, control 프로퍼티로 Field에 대한 제네릭타입을 주입받기 때문입니다. 이렇게해서 재사용성이 높고 any 타입을 사용하지 않으면서 타입체커로 좀 더 개발자 친화적으로 사용할 수 있는 MUI 기반의 form 컴포넌트를 만들어봤습니다.
전체 코드는 repository 에서 다운 받을 수 있으며, repository에는 MUI 의 Autocomplete 컴포넌트를 form 컴포넌트로 활용할 수 있는 예제가 포함되어 있습니다.
참고
https://ui.toast.com/weekly-pick/ko_20210505
리액트 컴포넌트를 타입스크립트 제네릭 함수처럼 쓰기
리액트 컴포넌트를 사용할 때 props 타입을 제네릭 패턴으로 정의하고, 정의한 타입들로 타입스크립트의 도움을 받아보자!
ui.toast.com
'FrontEnd > react' 카테고리의 다른 글
React 프로젝트 구조화 2 - atomic 디자인 적용의 한계와 반성 (0) | 2022.09.26 |
---|---|
React.forwardRef with typescript generics (0) | 2022.09.20 |
Typescript, MUI (Material UI v5), React hook form 함께 사용하기 (with Generic) (1/2) (0) | 2022.09.03 |
CRA 없이 React 프로젝트 구성하기 (0) | 2022.06.25 |
StoryBook 경로 설정하기 (0) | 2022.06.24 |