💭

RFH(react-hook-form)のヘルパーを理解したい(get)

2023/07/12に公開

目的

react-hook-formの実装内容を理解すること。
それに伴いコードリーディング力を上げること。

今回の対象

get.ts
import compact from './compact';
import isNullOrUndefined from './isNullOrUndefined';
import isObject from './isObject';
import isUndefined from './isUndefined';

export default <T>(obj: T, path?: string, defaultValue?: unknown): any => {
  if (!path || !isObject(obj)) {
    return defaultValue;
  }

  const result = compact(path.split(/[,[\].]+?/)).reduce(
    (result, key) =>
      isNullOrUndefined(result) ? result : result[key as keyof {}],
    obj,
  );

  return isUndefined(result) || result === obj
    ? isUndefined(obj[path as keyof T])
      ? defaultValue
      : obj[path as keyof T]
    : result;
};

実際に読んでいく

get<T>(obj: T, path?: string, defaultValue?: unknown): any

まず、関数名がgetなので、何かしらを取得したいとわかります。
具体的には、objからpathにマッチするものを取得したいのではないかと予想します。
返り値はanyなので具体的にはわかりません。

  if (!path || !isObject(obj)) {
    return defaultValue;
  }

pathがない場合orobjがObject型ではないときに、defaultValueを返しています。
なんで<T>に制約を持たせないんだろうか...?(誰かわかる方がいたらご教授ください)

  const result = compact(path.split(/[,[\].]+?/)).reduce(
    (result, key) =>
      isNullOrUndefined(result) ? result : result[key as keyof {}],
    obj,
  );

path.split()の引数に/[,[\].]+?/という正規表現をとることにより、.,,,[,]の文字があったときにstringを分割し、配列に格納していきます。
+?は、1つ以上の該当する文字が連続している場合にマッチするものだそうです。
この配列をcompact()[1]に代入することにより、falsyが取り除かれた配列が得られます。
さらにこの配列をreduce()します。コールバックの第一引数には累積値、第二引数には現在の値が入ってきます。(ちなみに、第三引数にはインデックス番号、第四引数には基の配列が入ってきます)
インデックス番号をiとします。

  1. i=0の時、
    resultには初期値のobjが入り、keyには、配列の要素の1番目が入ります。
    resultnullorundefinedの時は、result自体を返却し、そうではない時はresult[配列の要素の0番目]を返却します。
  2. i=1の時、
    resultにはi=0の時に返却された値が入り、keyには配列の要素の1番目が入ります。
    そして、i=0の時と同様の処理を行い、i=2,3,4,5...という様に処理をしていきます。
example.ts
const obj = {
 a: {
  b: {
   c: 'foo'
  }
 }
};
const path1 = 'a.b.c';
const path2 = 'a.b.d';

それぞれどうなるか考えてみてください。答えは一番下に書いておきます。

  return isUndefined(result) || result === obj
    ? isUndefined(obj[path as keyof T])
      ? defaultValue
      : obj[path as keyof T]
    : result;
  1. まず、isUndefined(result) || result === objにより、resultundefinedの場合、もしくはobjと同じ場合に、次の評価に移ります。
    違う場合は、resultが返却されます。(さっきの処理で探索が成功したということです)
  2. 次に、isUndefined[2](obj[path as keyof T])により、objのトップレベルに、keyに対応するvalueがあるかどうか調べます。valueがあった場合はその値を返し、なかった場合はdefaultValueを返して処理が終了します。
    なぜ2.の処理をするをするかというと、パスがトップレベルのプロパティを指している場合(つまり、ネストされたプロパティがない場合)、reduceは深く探索することなく終了するからです。

最後に

短いコードなのに難しかったです。reduceとか三項演算子のネストとか色々ややこしい、、、
いっちょまえに問題出していますが、僕も記事を書きながら学んでおり、アウトプットのためにやっていることなので、悪しからず...
間違い等ありましたらご教授いただけると幸いです。

脚注
  1. compact ↩︎

  2. isUndefined ↩︎

Discussion