SoYoung210 / soso-tip

🍯소소한 팁들과 정리, 버그 해결기를 모아두는 레포
24 stars 0 forks source link

react-hook-form 톺아보기 #53

Open SoYoung210 opened 3 years ago

SoYoung210 commented 3 years ago

react-hook-form의 동작방식은 HTML Element를 내부 ref로 관리하는 방식이다.


// useForm 'register'함수의 마지막 부분
return (ref: (TFieldElement & Ref) | null) =>
  ref && registerFieldRef(ref, refOrRegisterOptions);

// input의 ref를 attachEventListeners 함수에 전달한다.
function registerFieldRef(ref, options) {
  const fieldsRef = React.useRef<FieldRefs<TFieldValues>>({});
  const { name, type, value } = ref;

  // input type
  if (type) {
    attachEventListeners(
      field,
      isRadioOrCheckbox || isSelectInput(ref),
      handleChangeRef.current,
    );
  }
}

// blur와 change or input event를 등록한다.
export default function attachEventListeners(
  { ref }: Field,
  shouldAttachChangeEvent?: boolean,
  handleChange?: EventListenerOrEventListenerObject,
): void {
  if (isHTMLElement(ref) && handleChange) {
    ref.addEventListener(
      shouldAttachChangeEvent ? EVENTS.CHANGE : EVENTS.INPUT,
      handleChange,
    );
    ref.addEventListener(EVENTS.BLUR, handleChange);
  }
}

ref.addEventListener 를 통해 이벤트를 등록하고, UnMount(혹은 unregister)시점에 이벤트 리스너를 제거한다.

watch

입력의 onChangemode에서 유효성 검사가 필요할 때는 watch(or useWatch)를 사용한다.

const watchFieldsRef = React.useRef<InternalNameSet<TFieldValues>>(new Set());
const watchFields = watchId
  ? useWatchFieldsRef.current[watchId]
  : watchFieldsRef.current;

const assignWatchFields = (
  fieldValues: TFieldValues,
  fieldName: InternalFieldName<TFieldValues>,
  watchFields: Set<InternalFieldName<TFieldValues>>,
  ...
) => {
  let value = undefined;

  watchFields.add(fieldName);

  /* 생략 */
  return isUndefined(value)
    ? isSingleField
      ? inputValue
      : get(inputValue, fieldName)
    : value;
};

watch 를 수행하면 watchFields.add(fieldName); 에 의해 fieldName에 해당하는 input을 watchFieldsRef에 추가한다.

handleChangeRef.current = async ({ type, target }: Event) => {
  const name = (target as Ref)!.name;
  const field = fieldsRef.current[name];

  if (field) {
    /** 생략... */
    let shouldRender =
      !isEmptyObject(state) ||
      (!isBlurEvent && isFieldWatched(name as FieldName<TFieldValues>));
  }
};

이벤트 리스너에 등록되는 handleChangeRef.current 함수에서 isFieldWatched를 확인하여 shouldRender값을 결정한다. 이 값은 shouldRenderBaseOnError 함수에 전달되어 true일 경우 ReRendering을 발생시키는 updateFormState함수를 호출한다.

Controller

Controller는 외부 라이브러리 의존성 등으로 Controlled Component를 사용할 수 밖에 없는 경우 사용하는 Component이다.

const Controller = <
  TAs extends React.ReactElement | React.ComponentType<any> | NativeInputs,
  TFieldValues extends FieldValues = FieldValues
>(
  props: ControllerProps<TAs, TFieldValues>,
) => {
  const { rules, as, render, defaultValue, control, onFocus, ...rest } = props;
  const { field, meta } = useController(props);

  const componentProps = {
    ...rest,
    ...field,
  };

  return as
    ? React.isValidElement(as)
      ? React.cloneElement(as, componentProps)
      : React.createElement(as as NativeInputs, componentProps as any)
    : render
    ? render(field, meta)
    : null;
};

export { Controller };

useController

Controller와 같은 기능을 hooks로 사용할 수도 있다.

import { useController, useForm } from "react-hook-form";

function Input({ control, name }) {
  const {
    field: { ref, ...inputProps },
    meta: { invalid, isTouched, isDirty },
  } = useController({
    name,
    control,
    rules: { required: true },
    defaultValue: "",
  });

  return <TextField {...inputProps} inputRef={ref} />;
}

function App() {
  const { control } = useForm();

  return <Input name="firstName" control={control} />;
}
kwonth211 commented 3 years ago

안녕하세요 질문이 있습니다..! react-hook-form github 소스를 clone받고 봤는데도 올려주신 소스들은 찾을수가 없는데요.. 예를들어 watch 구현소스를 보고싶은데 어디서 볼 수 있을까요?

SoYoung210 commented 3 years ago

@kwonth211 안녕하세요,

본문의 코드와 관련 블로그 글이 react-hook-form v6를 기준으로 작성되어, v7인 지금은 많은 내용이 outdated된 것으로 보입니다.

이 글을 쓸때는 useForm return부터 하나하나 따라가는 식으로 파악했습니다. ㅎㅎ

kwonth211 commented 3 years ago

@SoYoung210 답변 감사합니다!