Published on

React hook formのソースコードを読む(その1)

Authors

はじめに

React hook formのソースコードを読んでみたので、その内容をまとめていきます。 毎度のことですが、自分の学習用備忘録ですので、間違いや 誤解があるかもしれません。その際は、ご指摘いただけると幸いです。

準備

適当な、作業ディレクトリを構築します。

npm create vite@latest rhf -- --template react-swc-ts
cd rhf
npm i
cd src
gh repo clone react-hook-form/react-hook-form

適当なReactが動くディレクトリを作成しました。 debuggerで止めて、ソースコードを読んでいきたいので、いつもこうしているのですが、絶対にもっと良い方法がある気がする。。。 OSS開発者の方とかどうやって、ライブラリの動作確認する環境を作ってるのだろうか???

(srcディレクトリ配下にないファイルを参照っしようとすると、↓のように怒られるので。。。)

which falls outside of the project src/ directory.

useForm()のコードを読んでみる

node_modulesからインポートすると、debuggerでstep intoで内部に入っていけないっぽいので、src配下に取り込んだリポジトリからimportします。

crate-react-appで作られたApp.tsxをそのまま流用し、useFormをインポート

// App.tsx
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { useForm } from "./react-hook-form/src/index";

function App() {
  const [count, setCount] = useState(0);

  e
  ;
  const { register } = useForm();

  return (

なぜかtypesのファイルでimportエラーが出るのでコメントアウト。

// src/react-hook-form/src/index.ts
export * from './controller';
export * from './form';
export * from './logic';
// export * from './types';
export * from './useController';
export * from './useFieldArray';
export * from './useForm';
export * from './useFormContext';
export * from './useFormState';
export * from './useWatch';
export * from './utils';

あとは、順に読んでいきます。

const _formControl = React.useRef // 省略
const _values = React.useRef // 省略
const [formState, updateFormState] = React.useState //省略

ref2つとstate1つを用意している。

  if (!_formControl.current) {
    _formControl.current = {
      ...createFormControl(props),
      formState,
    };
  }

!_formControl.currentは当然undefinedなので、createFormControlで初回のrefとなる値を作ってるっぽい。 L159で、formControlを返しているので、実質createFormControlがuseFormから返る値を作ってるっぽいので、後々しっかり読んでいく。

 return _formControl.current;
  useSubscribe({
    subject: control._subjects.state,
    next: (
      value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
    ) => {
      if (
        shouldRenderFormState(
          value,
          control._proxyFormState,
          control._updateFormState,
          true,
        )
      ) {
        updateFormState({ ...control._formState });
      }
    },
  });

なんか、サブスクライブしてるっぽい。subjectはcreateFormControlで作った、current.control._subject.state、nextはようわからんコールバックを渡してる。 よくわからんけど、ここは一旦スルー。

  React.useEffect(
    () => control._disableForm(props.disabled),
    [control, props.disabled],
  );

disableの処理をしてるっぽい。特に重要そうではないのでスルー。

 React.useEffect(() => {
    if (control._proxyFormState.isDirty) {
      const isDirty = control._getDirty();
      if (isDirty !== formState.isDirty) {
        control._subjects.state.next({
          isDirty,
        });
      }
    }
  }, [control, formState.isDirty]);

フォームが編集されたかどうかのハンドリングをするuseEffectっぽい。が、ここも一旦スルー。 こんな感じのvalues・errorsバージョンのuseEffectが続くが同じくスルー。

  React.useEffect(() => {
    if (!control._state.mount) {
      control._updateValid();
      control._state.mount = true;
    }

    if (control._state.watch) {
      control._state.watch = false;
      control._subjects.state.next({ ...control._formState });
    }

    control._removeUnmounted();
  });

↑ここだけ、依存配列なしのuseEffectなので、何がなんでも毎回発火させた模様。 control._removeUnmounted()でなんか大事な処理をしてそうなので、後で読む。

useForm関数自体は、ざっくり

  • 必要なstateとrefの作成
  • createFormControlで返り値の作成
  • useSubscribeの登録(よくわからない)
  • 各値・controlの変更を監視したuseEffectの処理

的なことをやってるっぽい。

createFormControlを読んでいけば、register周りの実装が書いてそうなので、createFormControlを読んでいく。

createFormControlを読んでいく

createFormControlは1429行ほどあるなかなか大きめのファイル。

...createFormControl(props), という使われ方をしてるので、L1356からの

  return {
    control: {
      register,
      unregister,
      getFieldState,
      handleSubmit,
      setError,
      _executeSchema,
      _getWatch,
      _getDirty,
      _executeSchema,
      _getWatch,
      _getDirty,
      _updateValid,
      _removeUnmounted,
      _updateFieldArray,
      _updateDisabledField,
      _getFieldArray,
      _reset,
      _resetDefaultValues,
      _updateFormState,
      _disableForm,
      _subjects,
      _proxyFormState,
      _setErrors,

からもわかるように、このファイルで、関数・変数などを呼び出し側で使えるように定義してあげてる模様。

公式ドキュメントに記載があまりされていないアンダーバー付きの関数もcontrolのメソッドとして提供されているので、アンダーバーなしの関数との差分が気になる。

registerを読んでいく(1回目のregister inputなどのエレメントに引数を渡す時に呼ばれる)

L997からregisterの定義が始まる。

面白いことに、registerの戻り値のrefの中でregisterを呼んでいる!!


    let field = get(_fields, name);
    const disabledIsDefined = isBoolean(options.disabled);

    set(_fields, name, {
      ...(field || {}),
      _f: {
        ...(field && field._f ? field._f : { ref: { name } }),
        name,
        mount: true,
        ...options,
      },
    });
    _names.mount.add(name);

    if (field) {
      _updateDisabledField({
        field,
        disabled: options.disabled,
        name,
        value: options.value,
      });
    } else {
      updateValidAndValue(name, true, options.value);
    }

処理自体は、とてもシンプル。

get(_fields, name)で、値を取得する。が、初回なので当然undefined。 (getはnameが、, ・[]・.なんかを含むpathのような文字列で渡ってきた時にオブジェクトからその値を良い感じに引っこ抜いてくれる関数)

set(getの逆バージョン)で、_fieldsのnameに第三引数をセットしていく。

_updateDisabledFieldはよくわからんけど、大事そうでもないのでスルー。 その後、L1023でデカ目のオブジェクトを返している。

registerを読んでいく (refが発火した時に呼ばれる)

ref発火時のregisterをステップ実行していきたいので、debuggerの位置を変更する。

  const returnVal = register("test");

  // 省略
        <input {...returnVal} ref={(e) => {
        debugger
        returnVal.ref(e);
      }} />

registerのrefにrefオブジェクトが渡ってくると、L1038が呼ばれる

      ref: (ref: HTMLInputElement | null): void => {
        if (ref) {
          register(name, options);

L1040でregisterが再度呼ばれる。

今回は、fieldがあるので、 updateValidAndValue ではなく、_updateDisabledField が呼ばれるところが違うくらい?

refがラジオボタンかどうかのチェックなんかをして、再度refの値込みで、setしている模様。

次回に続く

かなり雑にみていきましたが、弱弱エンジニアの私でも特段難しいこともなく読んでいけるので、非常に勉強になりそう。

次回はuseSubscribeあたりを読んでいこうと思います。