제어컴포넌트 : React에 의해 값이 제어되는 컴포넌트, useState hook을 통해 데이터를 관리함.
비제어 컴포넌트 : React에 의해 값이 제어되지 않는 컴포넌트, useREf를 통해 데이터를 관리함.
리렌더링이 발생하지 않기 때문에 필요에 따라 활용가능함.
const [input1, setInput1] = useState("");
const [input2, setInput2] = useState("");
const [input3, setInput3] = useState("");
...
const [input8, setInput8] = useState("");
const [input9, setInput9] = useState("");
const [input10, setInput10] = useState("");
// 값이 100개라면...???
const [inputs, setInputs] = useState({
input1:"",
input2:"",
...
input9:"",
input10:"",
}
// 음 묶으니까 나은데 계속해서 렌더링이 되야하네...?
제어컴포넌트로 form을 관리하면, 여러 state와 값을 변경하는 event Handler, 또 값을 검증하는 validation 이 존재함 => 코드양이 많고 관리하기 번거로움, 불필요한 리렌더링이 발생함.
react-hook-form 의 useForm hook을 사용하면 form을 쉽게 사용 할 수 있음, 주로 사용하는 props와 return 값들을 설명함
기본적으로 동작하는 방식은 비제어(uncontrolled) 방식으로 동작함. 비제어방식에서는 register 함수를 통해 react-hook-form이 input에 대한 값들을 추적하도록 도와줌- (불필요한 렌더링을 줄임, 성능이 좋다)
Controller 방식을 사용하면 MUI와 같은 라이브러리와 함께 사용가능함. (보통 React 관련 라이브러리들은 Controlled 방식으로 동작하기 때문)
export type UseFormProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = Partial<{
mode: Mode;
reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
defaultValues: DefaultValues<TFieldValues>;
resolver: Resolver<TFieldValues, TContext>;
context: TContext;
shouldFocusError: boolean;
shouldUnregister: boolean;
shouldUseNativeValidation: boolean;
criteriaMode: CriteriaMode;
delayError: number;
}>;
export type UseFormReturn<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
watch: UseFormWatch<TFieldValues>;
getValues: UseFormGetValues<TFieldValues>;
getFieldState: UseFormGetFieldState<TFieldValues>;
setError: UseFormSetError<TFieldValues>;
clearErrors: UseFormClearErrors<TFieldValues>;
setValue: UseFormSetValue<TFieldValues>;
trigger: UseFormTrigger<TFieldValues>;
formState: FormState<TFieldValues>;
resetField: UseFormResetField<TFieldValues>;
reset: UseFormReset<TFieldValues>;
handleSubmit: UseFormHandleSubmit<TFieldValues>;
unregister: UseFormUnregister<TFieldValues>;
control: Control<TFieldValues, TContext>; // controlled 방식으로 동작
register: UseFormRegister<TFieldValues>;
setFocus: UseFormSetFocus<TFieldValues>;
};
mode는 동작모드 설정 및 유효성 검사방법을 지정하는 option
onBlur(기본값) : 입력 필드의 유효성 검사가 입력 필드가 포커스를 잃을 때만 수행. 사용자가 입력 필드를 편집하고 다른 곳을 클릭하거나 탭할 때 유효성 검사가 실행됨. 이 모드는 사용자 경험을 향상시키고 입력 필드가 자주 변경되는 경우 유효성 검사를 줄이는 데 유용함
onChange : 입력 필드의 값이 변경될 때마다 즉시 유효성 검사가 수행됨. 사용자가 텍스트를 입력할 때마다 오류 메시지를 표시하거나 숨기고자 할 때 유용.(유효성 검사가 가장 많이 일어남 + 리렌더링 이슈 발생가능)
onSubmit : 폼 제출시에만 유효성 검사
const { register, handleSubmit, setValue } = useForm({
mode:"onChange",
defaultValues: {
fieldName1: 'Default Value 1',
fieldName2: 'Default Value 2',
},
});
비동기 유효성 검사를 수행하기 위해 사용함.
resolver는 폼 제출 시 실행되는 함수를 정의함. 이 함수는 입력데이터를 인자로 받아서 비동기적으로 유효성 검사를 실행하고 => Promise로 유효성 검사 결과를 반환함.
Promise가 resolve 되면 실행이 계속되고 아니면 중단됨.
react hook form과 함께 사용하는 resolver는 yup이 있음. 클라이언트단에서 yupResolver를 이용해 input값들을 서버로 보내기전 검증하는 과정을 거침.
스키마를 정의해서 정의한 스키마 기반으로 정보 검증
shape 속성을 사용하여 해당 객체에 대한 검사조건을 설정함.
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import * as yup from 'yup';
// Yup 스키마 정의
const schema = yup.object().shape({
firstName: yup.string().required('First name is required'),
lastName: yup.string().required('Last name is required'),
email: yup.string().email('Invalid email address').required('Email is required'),
});
function MyForm() {
const { control, handleSubmit, errors } = useForm({
resolver: yupResolver(schema), // Yup Resolver를 사용하여 유효성 검사를 설정합니다.
});
const onSubmit = (data) => {
// 유효성 검사를 통과한 데이터를 처리합니다.
schema.validate(data) 로 검사
성공시 .then(()=>{})
실패시 .catch((error) => {})
console.log(data);
};
react-hook-form에 입력요소를 등록하는데 사용함. register하는 과정을 통해 해당 input 값을 제어하고, 값을 수집, 유효성 검사를 실행 가능 (함수형태임)
첫번째 parmas는 name으로 해당 필드에 대한 key값
두번째는 optional 객체이고, 유효성 검사를 위한 프로퍼티들을 넣을 수 있음.
onChange handler를 바꾸는 것도 가능. optional 객체에 onChange 프로퍼티를 override 해주면 됨.
<input
type="text"
{...register("email", {
pattern: {
value:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
message: "이메일 형식에 맞지 않습니다.",
},
})}
<input
type="text"
name="fieldName"
ref={register} // 입력 요소를 등록합니다.
/>
//공식문서
const { onChange, onBlur, name, ref } = register('firstName');
// include type check against field path with the name you have supplied.
<input
onChange={onChange} // assign onChange event
onBlur={onBlur} // assign onBlur event
name={name} // assign name prop
ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />
setValue(name, value)형태로 사용함. key값에 대한 value를 등록함
watch : 폼에 입력된 값을 구독하여 실시간으로 체크 할 수 있게 해주는 함수. 매개변수를 주지 않으면 전체값을 관찰가능, 매개변수를 주면 해당 name의 값을 관찰가능함.
해당값에 따라 리렌더링을 발생시킴 => 폼 내에서 값이 변경할때마다 해당 값이 변함
getValues : 값을 반환하지만, 리렌더링을 발생하지않고 해당 값을 추적하지 않음.(그 순간의 값만 가져옴)
handleSubmit : submit 이벤트가 발생했을때, form태그에 onSubmit 이벤트 프로퍼티에 handleSubmit이라는 함수를 넣어주는 형태로 사용함
파라미터는 미리 정의한 submit에 대한 이벤트핸들러 함수를 넣어주면됨. e.preventDefault() 사용할 필요 없음.
setError, setFocus : submit이벤트 핸들러 함수에서 에러가 발생했다면, setError 함수를 사용해 에러를 발생시킬 수 있고, 에러가 발생한 필드에 setFocus를 통해 강조하기 가능
import React from 'react';
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, setValue, getValue, watch, setError, setFocus } = useForm();
// 폼 제출 핸들러
const onSubmit = (data) => {
const { username, password } = data;
// 실제 로그인 로직은 여기에서 수행될 수 있습니다.
// 예를 들어, 서버로 요청을 보내고 응답을 처리합니다.
if (username === 'user' && password === 'password') {
alert('로그인 성공!');
} else {
// 유효성 검사 오류를 설정합니다.
setError('password', {
type: 'manual',
message: '잘못된 사용자 이름 또는 비밀번호',
});
// 입력 필드에 포커스를 설정합니다.
setFocus('username');
}
};
// 'password' 필드의 값이 변경될 때마다 호출되는 함수
const onPasswordChange = () => {
const password = getValue('password');
// 비밀번호가 'password'인 경우 오류 메시지를 삭제합니다.
if (password === 'password') {
setError('password', {});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="username">사용자 이름:</label>
<input
type="text"
name="username"
id="username"
ref={register}
/>
</div>
<div>
<label htmlFor="password">비밀번호:</label>
<input
type="password"
name="password"
id="password"
ref={register}
onChange={onPasswordChange}
/>
{watch('password') === 'password' && (
<p style={{ color: 'red' }}>비밀번호를 변경하세요!</p>
)}
{errors.password && (
<p style={{ color: 'red' }}>{errors.password.message}</p>
)}
</div>
<button type="submit">로그인</button>
</form>
);
}
export default LoginForm;
useForm의 반환한값중 control이라는 속성이 존재
Controller라는 컴포넌트에 이 값을 전달하면 제어 컴포넌트로 react-hook-form을 사용가능(UI 라이브러리 들과 함께 사용할때 사용함)
useController hook이나 Controller라는 API 두가지를 모두 사용해서 구현 가능 (훅이냐 컴포넌트냐 차이..)
useController hook 사용추천 (재사용가능하기때문)
// 공식문서 Controller 예제
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Button } from '@mui/material';
function MyForm() {
const { control, handleSubmit, reset } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="name" // react-hook-form에서 사용할 필드 이름
control={control}
defaultValue="" // 초기값
render={({ field }) => (
<TextField
label="이름"
{...field}
/>
)}
/>
<Button type="submit">제출</Button>
</form>
);
}
export default MyForm;
Controller 컴포넌트를 사용하여 react-hook-form의 control을 전달하고, name, defaultValue, 그리고 UI 컴포넌트를 렌더링합니다.
UI 컴포넌트 (여기서는 TextField)는 render 함수 내에서 field 속성과 함께 전달됩니다. 이렇게 하면 react-hook-form이 필드를 관리하고 UI 컴포넌트와 연결됩니다.
이제 TextField에서 사용자가 입력한 데이터는 react-hook-form에서 처리되고 제출할 때 onSubmit 함수로 전달됩니다. 이 방법으로 react-hook-form와 UI 라이브러리를 함께 사용할 수 있으며, 폼 필드를 간단하게 관리할 수 있습니다.
import ReactDatePicker from "react-datepicker"
import { TextField } from "@material-ui/core"
import { useForm, Controller } from "react-hook-form"
type FormValues = {
ReactDatepicker: string
}
function App() {
const { handleSubmit, control } = useForm<FormValues>()
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
control={control}
name="ReactDatepicker"
render={({ field: { onChange, onBlur, value, ref } }) => (
<ReactDatePicker
onChange={onChange} // send value to hook form
onBlur={onBlur} // notify when input is touched/blur
selected={value}
/>
)}
/>
<input type="submit" />
</form>
)
}
<input {...register("name", {option})}/>
<input {...register("name2", {option})}/>
<input {...register("name3", {option})}/>
<input {...register("name4", {option})}/>
function InputText() {
return <input className="input"/>
};
<InputText {...register("name", {required: "반드시 입력해주세요."})}/> // error
// prop값을 정의안해줌.. register함수의 prop을 extend하거나 다른 방법이 필요함.
export type UseControllerProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
name: TName;
rules?: Omit<RegisterOptions<TFieldValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
shouldUnregister?: boolean;
defaultValue?: FieldPathValue<TFieldValues, TName>;
control?: Control<TFieldValues>;
};
export type UseControllerReturn<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
field: ControllerRenderProps<TFieldValues, TName>;
formState: UseFormStateReturn<TFieldValues>;
fieldState: ControllerFieldState;
};
control, name이라는 속성은 정보를 관리하는 부모컴포넌트로부터 받아옴.
name, control, rules 라는 parmas를 받음.
rules는 register option들.. 을 나타냄
import { TextField } from "@material-ui/core";
import { useController, useForm } from "react-hook-form";
function Input({ control, name }) {
const {
field,
fieldState: { invalid, isTouched, isDirty },
formState: { touchedFields, dirtyFields }
} = useController({
name,
control,
rules: { required: true },
});
return (
<TextField
onChange={field.onChange} // send value to hook form
onBlur={field.onBlur} // notify when input is touched/blur
value={field.value} // input value
name={field.name} // send down the input name
inputRef={field.ref} // send input ref, so we can focus on input when error appear
/>
);
}
react hook form에서 prop drilling을 방지하기 위해 useFormContext와 FormProvider를 사용 할 수 있다.
FormProvider를 통해 react-hook-form 관련 메소드를 사용할 컴포넌트를 감싸고, useFormContext를 호출해서 메소드를 가져온다.
FormProvider는 React Context기반으로 만들어졌다. => Context기반이기 때문에 전역상태가 변경되면 리렌더링이 일어나지만, memo를 이용해 isDirty 즉 사용자가 react hook form의 상태를 수정했을때만 컴포넌트가 리렌더링이 발생하도록 최적화 할 수 있다.
import React, { memo } from "react"
import { useForm, FormProvider, useFormContext } from "react-hook-form"
// we can use React.memo to prevent re-render except isDirty state changed
const NestedInput = memo(
({ register, formState: { isDirty } }) => (
<div>
<input {...register("test")} />
{isDirty && <p>This field is dirty</p>}
</div>
),
(prevProps, nextProps) =>
prevProps.formState.isDirty === nextProps.formState.isDirty
)
export const NestedInputContainer = ({ children }) => {
const methods = useFormContext()
return <NestedInput {...methods} />
}
export default function App() {
const methods = useForm()
const onSubmit = (data) => console.log(data)
console.log(methods.formState.isDirty) // make sure formState is read before render to enable the Proxy
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NestedInputContainer />
<input type="submit" />
</form>
</FormProvider>
)
}
ref) Blog : https://tech.osci.kr/introduce-react-hook-form
https://beomy.github.io/tech/react/react-hook-form/
https://velog.io/@leitmotif/Hook-Form%EC%9C%BC%EB%A1%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0
https://tech.osci.kr/react-hook-form-with-mui/
공식문서 : https://react-hook-form.com/get-started#Registerfields
https://velog.io/@boyeon_jeong/React-Hook-Form-Controller-useController