React Hooks(proposal)の予習をしたので自分用のメモをさらす

Hooksのalphaがきてやいのやいの言ってるので自分も追っておきますかということで予習。

この記事はReact v16.7.0-alpha時点の仕様と学習メモなので、Stableリリースされた時点でこの記事を参照にするのはよくないです。おそらくAPI変更が行われるはずなので。

ここではこの概要ページ以外のところに書かれているTipsとか詳細挙動とかFAQとかを中心にまとめて書いていく。基本的なことは概要ページで十分把握できると思う。

このエントリのモチベーションとしてはReactHooksの公式ガイドがいまのところ

  • 概要はこうです
  • それでは詳しくみていきましょう
  • 経験豊富な開発者はこう思うでしょう、これについては後述します
  • ここではこう書いていますが、これについてはFAQを参照してください
  • それではAPIリファレンスです、どうぞ
    • だがAPIリファレンスに具体的なことはあまり書いていない

という流れで、全部読まないと体系的に理解できない&情報があっちこっちに偏在しているという状態。

なので覚えておかないとあとあとハマりそうなことはこうして書いてリンク貼っておかないとわからなくなりそう。そういうモチベーションで書いている。

そういうわけであとあと忘れそうなことだけを書く。自分用のFAQともいう。あとコードブロックはほとんど公式ドキュメントからの引用。


useState

setHogeを使うときの挙動

classコンポーネントthis.setState()と違って、ステートはマージされるのではなくて置き換えになる。

useStateは浅く疎結に。あらゆるプロパティを1個にまとめないほうがよい

マージではなく置き換えになるので、ひとつ値が変わるたびにprevStateとマージする必要がでる。そもそも1個にまとめなかったらそんなことをしなくてもよいパターンって結構あるよねということらしい。

カスタムフックに置き換えたりするときも値のマージとかやってないから抽出が容易でよかったですねってなるらしい。

useEffect

どう便利?

  • classコンポーネント
    • componentDidMount,componentDidUpdateの2箇所に同一のコードを書く必要が出るパターンがある
    • ロジックを一箇所にまとめたい欲求がでてくる
    • Reactコンポーネントのバグ要因のひとつはcomponentDidUpdateでの適切な処理をミスることでもある
  • Effect Hook
    • ロジックをエフェクト1箇所にまとめられる
    • ブロッキングが起こらない
    • 基本的に毎レンダリング後に実行される
    • クリーンナップ処理ができる

ブロッキング起こらない

classコンポーネントだとcomponentDidMount,componentDidUpdateで副作用処理してると画面の更新をブロッキングするけど、useEffectはブロッキングしないらしい。

クリーンナップ処理

  • useEffectで関数を返すと、クリーンナップ実行時にその関数を実行してくれる
    • クリーンナップはいつ起こる?
      • 2度目のEffect時
        • useEffect()が実行されるとき前回のuseEffect()があれば、まず前回エフェクトのクリーンナップが行われる
          • useEffectレンダリング毎に異なるものが使用されているのはこのため
      • コンポーネントのアンマウント時にも実行される
function FriendStatus(props) {
  // ...
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

useEffect内でリターンした関数がクリーンナップ時に実行される

useEffectのパフォーマンス最適化

基本的に毎レンダリングごとにエフェクトは実行される。でも毎回発火するのは困るとかパフォーマンスがよくないとかそういうのが起こってくる。

classコンポーネントcomponentDidUpdateをこう書いていたものが

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

こうなる

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

エフェクトを1回だけ実行したいなら空の配列をわたす

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []);

Hooksのルール

reactjs.org

ここを読む

  • トップレベルでの呼び出しのみ
  • React Functionsからの呼び出しのみ

という基本ルールがある

ReactはuseStateによる複数のステートをどう認識しているのか

Hooksが呼び出された順番に依存して認識している

条件分岐

useEffectの条件分岐をしたい場合はuseEffect内でif文

 useEffect(function persistForm() {
    // 👍 We're not breaking the first rule anymore
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

ifブロックの中でuseEffectしてはいけない

ifブロックのなかにuseEffectを入れて条件によってuseEffectの実行が歯抜けになったりすると、Reactは困ってしまう。

なぜなら前述したとおりReactはHooksの呼び出された順番に依存して認識しているので、歯抜けになると順番が破綻してバグってしまう。

これらルールを守っていくには

eslintのルールプラグインが提供されているのでこれを使うと安心安全

www.npmjs.com

インラインコールバックで再レンダリングが起こる問題を防ぐには

classコンポーネントthis.onClick = this.onClick.bind(this)として回避していたようなやつをHooksではどうやるか。

useCallbackuseReducerを使う。

useCallback

この形式

function HelloButton(props) {
  const handleClick = useCallback(()=> console.log('hello'), [])

  return (
    <button onClick={handleClick}>Greeting</button>
  );
}

useReducer

この形式

function AddButton(props) {
  const [todos, dispatch] = useReducer(todosReducer);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}

React的にはネストが深いコンポーネントにコールバックをバケツリレーしなくてはいけない場合は、useContextを使ってdispach関数を子コンポーネントで扱えるようにするパターンがグッドだよねという雰囲気を感じる(https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down)。

そのほか「〜するべき?」「どうすれば〜できる?」などの疑問

reactjs.org

身も蓋もないけどほかはだいたいFAQ読むとだいたい書かれている。

結構想定されるQuestionがしっかりカバーされている印象。

ただAPIの使い方や挙動を知った上でないと理解できないものがほとんどなので、そういう意味でも当エントリの上述部分を把握しておくとFAQが読みやすくなると思っている。


なんだこの雑なエントリは!