How to create reusable form components with React Hook Forms and Typescript - This Dot Labs
Why Should I Create Reusable react-hook-form Input Components? Like many in the React community, you've decided to use react-hook-form. While not every…
www.thisdot.co
완성된 코드 보기
https://stackblitz.com/edit/reusable-rhf-ts-pt6?file=src%2Fcomponents%2Fatoms%2Finput.tsx
Reusable React Hook Form Typescript Components - Live Example - StackBlitz
stackblitz.com
Step 1:Input 컴포넌트 만들기
import React, {
FC,
forwardRef,
DetailedHTMLProps,
InputHTMLAttributes,
} from 'react';
import classNames from 'classnames';
export type InputSize = 'medium' | 'large';
export type InputType = 'text' | 'email';
export type InputProps = {
id: string;
name: string;
label: string;
type?: InputType;
size?: InputSize;
className?: string;
} & Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size'
>;
// Using maps so that the full Tailwind classes can be seen for purging
// see https://tailwindcss.com/docs/optimizing-for-production#writing-purgeable-html
const sizeMap: { [key in InputSize]: string } = {
medium: 'p-3 text-base',
large: 'p-4 text-base',
};
export const Input: FC<InputProps> = forwardRef<HTMLInputElement, InputProps>(
(
{
id,
name,
label,
type = 'text',
size = 'medium',
className = '',
placeholder,
...props
},
ref
) => {
return (
<input
id={id}
ref={ref}
name={name}
type={type}
aria-label={label}
placeholder={placeholder}
className={classNames([
'relative inline-flex w-full rounded leading-none transition-colors ease-in-out placeholder-gray-500 text-gray-700 bg-gray-50 border border-gray-300 hover:border-blue-400 focus:outline-none focus:border-blue-400 focus:ring-blue-400 focus:ring-4 focus:ring-opacity-30',
sizeMap[size],
className,
])}
{...props}
/>
);
}
);
https://stackblitz.com/edit/reusable-rhf-ts-pt6?file=src%2Fcomponents%2Fatoms%2Finput.tsx
Reusable React Hook Form Typescript Components - Live Example - StackBlitz
stackblitz.com
Step 2: Creating a Form
import React, { FC } from 'react';
import { Input } from '../atoms/input';
export const RegistrationForm: FC = () => {
return (
<form>
<Input
id="firstName"
type="text"
name="firstName"
label="First Name"
placeholder="First Name"
/>
</form>
);
};
firstName 필드를 필수로 만들고, 검증 로직을 추가하고 싶습니다.
그리고 양식을 제출하는 방법을 추가해야 합니다.
<RegistrationForm>에서 useForm 호출에서 handleSubmit 함수를 구조 분해로 꺼내온 다음
이를 사용하여 양식 제출을 위해 호출할 onSubmit 함수를 생성합니다.
Reusable React Hook Form Typescript Components - Live Example - StackBlitz
stackblitz.com
import React, { FC } from 'react';
import { useForm } from 'react-hook-form';
import { Input } from '../atoms/input';
export type RegistrationFormFields = {
firstName: string;
};
export const RegistrationForm: FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegistrationFormFields>();
const onSubmit = handleSubmit((data) => {
console.log('submitting...');
});
return (
<form onSubmit={onSubmit}>
<Input
id="firstName"
type="text"
name="firstName"
label="First Name"
placeholder="First Name"
/>
<button
className="mt-4 transform duration-200 py-2 px-4 bg-blue-500 text-white font-semibold rounded shadow-md hover:bg-blue-600 focus:outline-none disabled:opacity-50 focus:translate-y-1 hover:-translate-y-1"
type="submit"
>
Submit
</button>
</form>
);
};
Step 3: Create a Validated Input Component
import React from 'react';
import { Input, InputProps } from '../atoms/input';
export type FormInputProps = InputProps;
export const FormInput = ({
className,
...props
}: FormInputProps): JSX.Element => {
return (
<div className={className} aria-live="polite">
<Input {...props} />
</div>
);
};
https://stackblitz.com/edit/reusable-rhf-ts-pt6?file=src%2Fcomponents%2Fmolecules%2Fform-input.tsx
Reusable React Hook Form Typescript Components - Live Example - StackBlitz
stackblitz.com
react-hook-form에 Input 등록하기
import React from 'react';
import { InputProps } from '../atoms/input';
export type FormInputProps = {
register: ???; // 뭘까요?
} & InputProps;

import React from 'react';
import { InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
register: ???;
} & InputProps;
import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
register?: UseFormRegister<TFormValues>;
};
name Property 정의하기
// 아래 타입의 Path
export type RegistrationFormFields = {
firstName: string;
lastName: string;
email: string;
bio?: string;
};
export type RegistrationFormFields = {
people: { firstName: string }[];
};
import React from 'react';
import { UseFormRegister, Path } from 'react-hook-form';
import { InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
name: Path<TFormValues>;
register?: UseFormRegister<TFormValues>;
} & InputProps;
import React from 'react';
import { UseFormRegister, Path } from 'react-hook-form';
import { InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
name: Path<TFormValues>;
register?: UseFormRegister<TFormValues>;
} & Omit<InputProps, 'name'>;
유효성 검사 규칙을 <FormInput>에 전달한 다음 register 함수에 전달합니다.
Rules Property 정의하기
우리는 등록 폼에 적용되는 규칙뿐만 아니라 모든 규칙을 <FormInput> 컴포넌트에 전달할 수 있기를 원한다는 것을 기억하십시오.
고맙게도 이것은 조금 더 쉽습니다.
import React from 'react';
import { UseFormRegister, Path, RegisterOptions } from 'react-hook-form';
import { InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
name: Path<TFormValues>;
rules?: RegisterOptions; // here!
register?: UseFormRegister<TFormValues>;
} & Omit<InputProps, 'name'>;
Reusable React Hook Form Typescript Components - Live Example - StackBlitz
stackblitz.com
Errors Property 정의
Error property를 정의할 때 오류에 어떤 타입을 할당해야 하나요?
react-hook-form이 말하는 Error type이 무엇인지 살펴보겠습니다.
{
firstName?: FieldError;
}
import React from 'react';
import {
RegisterOptions,
DeepMap,
FieldError,
UseFormRegister,
Path,
} from 'react-hook-form';
import { InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
name: Path<TFormValues>;
rules?: RegisterOptions;
register?: UseFormRegister<TFormValues>;
errors?: DeepMap<TFormValues, FieldError>;
} & Omit<InputProps, 'name'>;


...
export type FormInputProps<TFormValues> = {
...
errors?: DeepMap<TFormValues, FieldError>;
} & Omit<InputProps, 'name'>;
...
export type FormInputProps<TFormValues> = {
...
errors?: Partial<DeepMap<TFormValues, FieldError>>;
} & Omit<InputProps, 'name'>;
이것은 오류가 TFormValues의 모든 필드를 포함할 필요가 없다는 것을 의미하며,
Typescript 오류는 이제 사라졌습니다!
에러 표시하기
export const FormInput = <TFormValues extends Record<string, unknown>>({
name,
register,
rules,
errors,
className,
...props
}: FormInputProps<TFormValues>): JSX.Element => {
const errorMessages = errors[name];
const hasError = !!(errors && errorMessages);
return (
<div className={className} aria-live="polite">
<Input name={name} {...props} {...(register && register(name, rules))} />
{hasError && <p>{errorMessages}</p>}
</div>
);
};
export const FormInput = <TFormValues extends Record<string, unknown>>({
name,
register,
rules,
errors,
className,
...props
}: FormInputProps<TFormValues>): JSX.Element => {
// If the name is in a FieldArray, it will be 'fields.index.fieldName' and errors[name] won't return anything, so we are using lodash get
const errorMessages = get(errors, name);
const hasError = !!(errors && errorMessages);
return (
<div className={className} aria-live="polite">
<Input name={name} {...props} {...(register && register(name, rules))} />
{hasError && <p>{errorMessages}</p>}
</div>
);
};
npm install @hookform/error-message 또는 yarn add @hookform/error-message
import React from 'react';
import classNames from 'classnames';
import get from 'lodash.get';
import {
RegisterOptions,
DeepMap,
FieldError,
UseFormRegister,
Path,
} from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { Input, InputProps } from '../atoms/input';
export type FormInputProps<TFormValues> = {
name: Path<TFormValues>;
rules?: RegisterOptions;
register?: UseFormRegister<TFormValues>;
errors?: Partial<DeepMap<TFormValues, FieldError>>;
} & Omit<InputProps, 'name'>;
export const FormInput = <TFormValues extends Record<string, unknown>>({
name,
register,
rules,
errors,
className,
...props
}: FormInputProps<TFormValues>): JSX.Element => {
// If the name is in a FieldArray, it will be 'fields.index.fieldName' and errors[name] won't return anything, so we are using lodash get
const errorMessages = get(errors, name);
const hasError = !!(errors && errorMessages);
return (
<div className={className} aria-live="polite">
<Input
name={name}
aria-invalid={hasError}
className={classNames({
'transition-colors focus:outline-none focus:ring-2 focus:ring-opacity-50 border-red-600 hover:border-red-600 focus:border-red-600 focus:ring-red-600':
hasError,
})}
{...props}
{...(register && register(name, rules))}
/>
<ErrorMessage
errors={errors}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name={name as any}
render={({ message }) => (
<p className="mt-1 font-serif text-sm text-left block text-red-600">
{message}
</p>
)}
/>
</div>
);
};
결론
Reusable React Hook Form Typescript Components - Live Example - StackBlitz
stackblitz.com
'FrontEnd' 카테고리의 다른 글
CSS : Flex와 min-width (0) | 2022.06.30 |
---|---|
zustand와 타입스크립트 [공식문서번역] (0) | 2022.06.29 |
리액트 라우터 v6를 이용해 쉽게 모달 만들기 (0) | 2022.06.27 |
리액트 테스트 : 폼(Form) 테스트 (0) | 2022.06.26 |
리액트 테스트 : implementation details을 피하기 (0) | 2022.06.26 |