React

Formik -> React-Hook-Form 리팩토링

mykuromi 2024. 6. 10. 20:47

 

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}
    />
  );
};