22行だけでReactのHooksを実装する!


React 16.8で使えるようになったHooks APIは、人によって魔法にしか見えない。しかし、Hooksの背景にあるアイデアは本当はとても簡単!この短い投稿を読み終わった頃には、Hooksは本質的に何なのかを理解できるはず。

注意: この投稿を読む人には、Javascriptと多少のReactの知識があるという前提で作っています

ということで、この投稿(英語)を参考にしながら、useEffectuseStateをゼロから実装してみよう。

useStateuseEffect

const MyReact = (function() {
  let hooks = [] // Hooksの配列
  let currentHook = 0 // イテレータ
  return {
    useEffect(callback, depArray) {
      // depArrayは、エフェクトを実行すべきかどうか判断するために与えられる値の配列
      const hasNoDeps = !depArray // depArrayがundefinedの場合True, それ以外False
      const deps = hooks[currentHook] // Array | undefined
      // depsがundefinedじゃない場合、前回レンダーと比べて配列の中の値が変わったかをチェック
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        // depsの配列がない場合とdepsが変わった場合は、毎回レンダー時にエフェクトを実行
        callback()
        hooks[currentHook] = depArray // 最新のdepArrayを覚える
      }
      currentHook++ // 次のHookに進む
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // currentHookをそのまま使うと、クロージャのせいでsetStateが実行されたとき、
      // インデックスがコンポーネントのレンダー順番と一致することを保証できないので、 
      // 変わらない値にインデックスを保存する
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState] // 値と関数を戻し、次のHookに進む
    }
  }
})()

以上!コメントを除けば、この22行だけで、Hooksが実装できた!

公式のHooksのコードを見てみると、一見だいぶ違うように見えるが、それはReactのFiber設計の独特な実装がかかわっているからで、関数の定義をよく調べれば、上記の実装にかなり近いことがわかる。

一番注目すべきところは、イテレータの存在。
Hooksを使うときは、それぞれのuseStateは、なぜ正しいコンポーネントのステートがわかるのか?という疑問を持ったことがあるだろう。実は、Reactがレンダーの順番にHookの保存を合わせているだけだった。ひとつのHooksのルールとしては、「条件やループの中でuseStateuseEffectを呼ばない」があるのは、そのためだ。開発者が作る条件は、Reactの範囲外なので、正しい値を返すことを保証できない。

補足

厳密にいえば、HooksはReactとはまったく関係がない。上記のコードを見てみると、React特有のコードが一行もなく、やろうと思えば、別のライブラリ、別の目的で活かせることがわかる。実は、React以外のところで使うためのHookライブラリを、@getifyが作っている。Githubリンク:https://github.com/getify/TNG-Hooks