Formik -> React-Hook-Form 리팩토링
✓ Formik : https://formik.org/docs/overview
✓ React-Hook-Form : https://react-hook-form.com/get-started
원래 프로젝트에 설치되어 있던 MUI + Formik 라이브러리로 전체적인 Form 페이지를 만들다가, textarea 입력 부분에서 버벅임이 심해 어떻게 할 지 고민하고 있었다.
구글링을 해보니 Formik은 controlled component라, form 값이 변하면 form 전체 상태값이 변하고 폼 전체가 다시 렌더링되기 때문에 느려지는 것.
React 개발자 도구를 켜서 성능 체크를 해보니, textarea 부분만 입력을 해도 폼 전체가 노란색으로 변하는 것(렌더링)을 볼 수 있었다.
실제 타이핑 후 입력될 때 까지 약 1초 정도 걸린 것 같다.
도저히 이렇게 만들어선 안될 것 같아서 좀 더 빠른 React-hook-form으로 교체.
다른 탭 페이지는 React-hook-form으로 만들어서 비교해보니 더 빨라진 것을 체감할 수 있었다.
Formik을 사용해서 입력할 때의 버벅임은 전혀 없었다.
Login page와 같이 ID/PW 정도만 있는 간단한 폼이나 유효성 검사가 주 목적인 폼은 Formik을 사용해도 좋을 것 같지만, input 요소만 10개가 넘는 폼은 React-hook-form을 사용하면 좋을 것 같다.
✓ Formik + MUI
- FORM 전체에서 조회해 온 데이터를 Redux에 담아왔고, forEach로 formikRef.current.setFieldValue를 써서 코드 몇 줄로 데이터를 뿌려줄 수 있어서 편했다.
- MUI랑 같이 사용했는데, Formik 도 MUI도 controlled component 형식이라 같이 쓰면 어울리는 것 같긴 하다.
- <Formik> 으로 감싸진 각 input의 value값을, setFieldValue를 통해 전체의 Formik state로 관리
-> 폼 안의 하나의 input 요소만 수정해도, 전체가 다시 렌더링되는 방식이다.
export const RecordDetail = () => {
const RecordData = useSelector(selectRecord);
const dispatch = useDispatch();
const formikRef = useRef();
const printRef = useRef();
// paga load init
useEffect(() => {
dispatch(resetDocNo());
}, []);
useEffect(() => {
if (!RecordData || !formikRef.current) {
return;
}
// set popup data
Object.keys(RecordData).forEach((key) => {
if (RecordData[key]) {
formikRef.current.setFieldValue(key, RecordData[key]);
}
});
}, [RecordData]);
return (
<Formik
innerRef={formikRef}
initialValues={{ ...RecordData }}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
setStatus({ success: false });
setSubmitting(true);
dispatch(saveDocNo(values));
setSubmitting(false); // enable submit button
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}}
>
{({ handleBlur, handleChange, handleSubmit, isSubmitting, values, setFieldValue }) => {
return (
<form noValidate onSubmit={handleSubmit}>
<Stack id="id-wrapper" spacing={2}>
<Stack id="id-button-row" direction="row" spacing={2} justifyContent="flex-end">
<Button name="save" type="submit" color="secondary" variant="contained" disabled={isSubmitting} onSubmit={handleSubmit}>
Save
</Button>
<Button name="delete" color="secondary" variant="contained">
Delete
</Button>
</Stack>
<Stack direction="row" spacing={2}>
<TextField
name="docNo"
label="Doc No"
variant="outlined"
value={values.docNo}
sx={{ flex: 1 }}
InputProps={{
readOnly: true,
}}
/>
<Button
name="search"
variant="secondary"
disabled={isSubmitting}
sx={{ minWidth: 0, width: 40, padding: 0 }}
>
<SearchIcon color="secondary" />
</Button>
</Stack>
<Stack direction="row" spacing={2}>
<TextField
name="machineryCode"
label="Machinery"
variant="outlined"
value={values.machineryCode}
InputProps={{
readOnly: true,
}}
sx={{ width: searchCodeRatio }}
/>
<Autocomplete
disableClearable
name="machieneryNo"
options={[values.machieneryNo]}
value={values.machieneryNo}
onChange={(event, value) => setFieldValue('machieneryNo', value)}
isOptionEqualToValue={(option, value) => option.id === value.id}
sx={{ flex: 1 }}
renderInput={(params) => <TextField {...params} label="" />}
/>
<TextField
name="machineryDesc"
variant="outlined"
value={values.machineryDesc}
InputProps={{
readOnly: true,
}}
sx={{ width: searchDescRatio }}
/>
</Stack>
<Divider />
<Stack direction="row" spacing={2}>
<Autocomplete
name="deck"
options={[
{ id: '', label: '' },
{ id: 'd', label: 'Deck' },
{ id: 'e', label: 'Engine' },
]}
value={values.deck}
onChange={(event, value) => setFieldValue('deck', value)}
isOptionEqualToValue={(option, value) => option.id === value.id}
sx={{ width: leftStackRatio }}
renderInput={(params) => <TextField {...params} label="Deck" />}
/>
<TextField
name="voyage"
label="Voyage"
value={values.voyage}
onBlur={handleBlur}
onChange={handleChange}
sx={{ width: rightStackRatio }}
/>
</Stack>
<Stack direction="row" spacing={2}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DemoContainer components={['DatePicker']} sx={{ width: leftStackRatio }}>
<DatePicker
name="workDate"
label="Work Date"
format="YYYY-MM-DD"
value={dayjs(values.workDate)}
onChange={(value) => setFieldValue('workDate', value.toDate())}
sx={{ flex: 1 }}
/>
</DemoContainer>
</LocalizationProvider>
</Stack>
/* 코드 생략 */
<Stack direction="row" spacing={2}>
<TextField
name="workDetail"
label="Work Detail"
multiline
rows={10}
value={values.workDetail}
onBlur={handleBlur}
onChange={handleChange}
sx={{ flex: 1 }}
/>
</Stack>
</Stack>
</form>
);
}}
</Formik>
);
};
✓ React-hook-form + MUI
- MUI로 작성된 코드가 너무 클린하지 못해서, React-hook-form control 컴포넌트 만드는 김에 같이 분리.
- MUI는 controlled 방식인데, React-hook-form은 uncontrolled(ref) 방식이다.
- uncontrolled component는 즉, 항시 state로 관리되지 않고 제출 버튼을 누를 때 값을 읽어서 서버에 전송하는 방식.
-> custom component에 useController를 사용해서 value가 제어될 수 있도록 변경했다.
export const Spare = () => {
const spareData = useSelector(selectSpare);
// react hook form
const {
control,
handleSubmit,
formState: { isSubmitting, isSubmitted, errors },
} = useForm({
defaultValues: spareData,
});
const onSubmitSuccess = (data) => {
console.log(data); // object with values
};
const onSubmitFail = (data) => {
console.log(data); // object with error event
};
return (
<form onSubmit={handleSubmit(onSubmitSuccess, onSubmitFail)}>
<Stack spacing={2}>
<Stack direction="row" spacing={2} justifyContent="flex-end">
<Button name="save" type="submit" color="secondary" variant="contained">
Save
</Button>
<Button name="delete" color="secondary" variant="contained">
Delete
</Button>
</Stack>
<Stack direction="row" spacing={2}>
<CustomTextField control={control} name="docNo" label="Doc No" readOnly={true} sx={{ flex: 1 }} />
<Button
name="search"
variant="secondary"
onClick={(event) => event.preventDefault()}
sx={{ minWidth: 0, width: 40, padding: 0 }}
>
<SearchIcon color="secondary" />
</Button>
</Stack>
<Stack direction="row" spacing={2}>
<CustomTextField control={control} name="machineryCode" label="Machinery" readOnly={true} sx={{ width: searchCodeRatio }} />
<CustomAutocomplete control={control} name="machieneryNo" sx={{ flex: 1 }} />
<CustomTextField control={control} name="machineryDesc" readOnly={true} sx={{ width: searchDescRatio }} />
</Stack>
<Stack direction="row" spacing={2}>
<CustomTextField control={control} name="equipmentCode" label="Equipment" readOnly={true} sx={{ width: searchCodeRatio }} />
<CustomAutocomplete control={control} name="equipmentNo" sx={{ flex: 1 }} />
<CustomTextField control={control} name="equipmentDesc" readOnly={true} sx={{ width: searchDescRatio }} />
</Stack>
<Stack direction="row" spacing={2}>
<CustomTextField control={control} name="measurement" label="Measurement" readOnly={true} sx={{ flex: 1 }} />
</Stack>
/* 코드 생략 */
<Stack direction="row">
<CustomDataGrid control={control} name="datagrid" />
</Stack>
</Stack>
</form>
);
};
import { useController } from 'react-hook-form';
// material-ui
import { TextField } from '@mui/material';
export const CustomTextField = ({ control, name, label = '', readOnly = false, required = false, ...others }) => {
const {
field,
fieldState: { invalid, isTouched, isDirty },
formState: { defaultValues. touchedFields, dirtyFields },
} = useController({
name,
control,
rules: { required: required },
});
return (
<TextField
variant="outlined"
label={label}
name={field.name}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
// inputRef={field.ref}
readOnly={readOnly}
InputProps={{
readOnly: readOnly,
}}
{...others}
/>
);
};