React Hooksとは?

hooksreact

この記事を通じて伝えたいメッセージ

  • React Hooksは「Reactの機能を活用できる関数」である。
  • React Hooksは「関数コンポーネントでのみ使用できる」。
  • React Hooksは「クラスコンポーネントの欠点を補うために作られた」。
  • React Hooksは「make it easier build great UI」という哲学に基づいて作られた。

動機

Reactを使いながら、核心機能の一つであるHooksについてちゃんと考えたことがなかった。

航海プラスを進める中で、React Hooksの原理を深く理解し、直接実装してみる機会ができ、次のような動画を見ることになった。

学習を始めながら上の動画を見ることになり、この機会にしっかり整理してみようと思う。

Reactの開発哲学

2018 REACT CONF 発表内容
2018 REACT CONF 発表内容

Reactは「make it easier build great UI」という哲学に基づいて作られた。

そして、このような哲学に基づいてReactチームはReactの機能を継続的に発展させてきた。

このような背景を先に言及する理由は、この哲学から開発された機能がまさにHooksだからである。

私が考えるフック

私はフックを「Reactの機能を活用できる関数」だと考えている。

Reactの機能を使用するにはいくつかの制約があり、Lintなどの使用のためにuseプレフィックスを付けなければならない規約があるが、それでも結局React次元での関数だと考えている。

そのため、関数でコードを分離した時の利点を一部共有する。

フック(Hooks)の語源

「関数と表現すればいいのではないでしょうか?フックと表現したのには理由があるのではないでしょうか?」

専門家として用語を明確にすることは重要だと思う。

そして、Reactチームはフックを「Hooks」と表現している。

その理由は何だろうか?

Hooks are functions that let you "hook into" React state and lifecycle features from function components.

React公式ドキュメント

React公式ドキュメントでは上記のように説明している。

Hookは関数コンポーネントからReactの状態とライフサイクル機能に「フック」できる関数です。

つまり、フックを引っ掛ける。という意味で「Hooks」という用語を使用しているのである。

フックを引っ掛ける
フックを引っ掛ける

フックが解決しようとする問題

言葉がピンとこないかもしれないが、フックがどのような問題を解決しようとしたのかを見ると助けになると思う。

React Hooksはクラス型から関数型へReact作成のパラダイムが移行する過渡期に登場した。

既存のクラス構文では、Reactのライフサイクルに関連した次の機能を持っていた。

Reactライフサイクル
Reactライフサイクル

Reactコンポーネントに関して、より細かく状態に介入できるという利点があるが、この利点は同時に欠点にもなった。

  • コンポーネント間で状態ロジックを再利用するのが難しい。
  • 複雑なコンポーネントは理解するのが難しい。
  • クラスは人と機械を混乱させる。

出典:React公式ドキュメント - Hookの概要

これ以外にも、複雑なライフサイクルにより不要なコードが多くなるという問題もあった。

Reactの開発哲学で言及したように、Reactチームはこのような問題を解決するためにフックを導入した。

コードを通じて見てみよう

コードで見た問題 - 1

動機で紹介したDemoコードを基に問題を見てみよう。

ここではクラス型と関数型で使用する時の違い、そしてフックがどのように使用されるかを見る。

JSX

このコードを基に説明する予定だ。

ここでの目標は「Mary」という名前を自分で画面上の入力で変更することだ。

今のように関数コンポーネントが一般化されておらず、classがほとんどだった時期には次のようにコードを書いていた。

JSX

もし、この状況でclassを使わずにstateの使用を望むならどうすればいいだろうか?

私たちは当たり前のようにuseStateを言うだろうが、忘れないでほしい。これはuseStateが登場する前、またはその瞬間の話である。

JSX

同じ機能を関数型コードで作成した。この時、useStateというhookを使用した。

比較のためにTabにクラス型コードも一緒に作成した。2つのアプローチを比較しよう。

クラス型関数の場合、次のような特徴がある。

  • stateがオブジェクトとして宣言される。そして関連要素はオブジェクトのプロパティとして存在する。
  • eventHandlerの場合、stateアクセスのためにthisにバインドされる必要がある。
  • 状態の値にアクセスするにはthis.state.nameのように使用しなければならない。

一方、hooksに基づいた関数型を使用する場合、複雑な過程が必要ない。

useStateを通じてstateを宣言し、setStateを通じて状態を変更できる。

A Hook is a function provided by react that lets you hook into react features from your function component.

最初にフック(Hooks)の語源で言及したように、フックはReactの機能を関数コンポーネントで使用できるようにする関数である。

useStateもReactが提供する関数と見ればいい。

さて、surnameという項目が追加されたと考えてみよう。

JSX

DX的に関数型の方がより簡単に修正できることが感じられるだろうか?

ここで、クラス型の最大の問題は関心事が完全に分離されていないことだ。

stateオブジェクト内にnamesurnameという2つの状態が混在しているのだ。

コードで見た問題 - 2

今回はcontextで見てみよう。

context, it's like kind of like global variables for a subtree so it's useful for things like read the current theme like visual theme or current language that the user is using and it's useful to avoid passing everything through props if you need all components to be able to read some value.

動画に出てきた内容で、subtreeに対するグローバル変数として簡単に表現できる。

別の言葉でグローバルステートとも表現できる。

JSX

作成されたコードは上記のようで、クラス型と関数型を比較してみよう。

クラス型関数型
どのような過程を実行すべきか明確に記述される。(命令型)何をするか明確に記述される。(宣言型)
やや複雑にロジックが絡み合っている。(nested)より簡単に記述されている。(flat)
出力部に状態処理ロジックが一緒に含まれている。出力部と状態処理ロジックがより明確に分離されている。

フック(hooks)の規則

クラス型と違って関数型ではstateが分離されているが、これがどのように可能なのか疑問を持つかもしれない。

関数型では宣言型でどのような結果になるか!と言い、過程を明確に指定しないのにどのようにスムーズに動作するのかという疑問だ。

Reactは関数呼び出し順序に依存する。

少し馴染みのない言葉かもしれないが、簡単に考えると、フック(hooks)を使用するには守るべき規則があると見ればいい。

  1. フックはコンポーネントの最上位でのみ呼び出されなければならない。
  2. フックは関数コンポーネント内でのみ呼び出されなければならない。

この規則を守らないと、Reactが予期しない動作をする可能性がある。

JSX

上記のようにif文の中でuseStateのようなフックの呼び出しを許可しない。

これを防ぐためにReactチームはLint Pluginを提供している。

コードで見るライフサイクル管理

フックが解決しようとする問題で扱ったライフサイクルに関連した問題をコードを基に見てみよう。

JSX

コンポーネントがレンダリングされる時にdocument.titleを変更するようにコードを修正した。(タブのタイトル修正)

クラス型ではcomponentDidMountcomponentDidUpdateを使用したが、関数型ではuseEffectを使用した。

useEffectは関数コンポーネントでside effectを実行できるようにする。

ここで核心的に見るべきことは、クラス型ではライフサイクルに応じたメソッド名をすべて考慮しなければならなかったが、関数型ではuseEffect一つですべてを解決できる点だ。

つまり、フックを使用することで過程が非常に簡単になった。

レスポンシブウェブのようにコンポーネントの状態が変わるたびにdocument.titleを変更しなければならない場合も見てみよう。

JSX

ここで核心的に見るべき点は、クラス型ではロジック追加のためにコードの複数の場所を修正しなければならなかったという点だ。

一方、関数型の場合は一箇所にまとめて管理できるだけでなく、ライフサイクルに応じたメソッドをすべて覚える必要なくuseEffect一つで解決できるという点だ。

同時に、関心を持つ要素ごとにuseEffectを作成することで、関心事の分離(separation of concerns)ができる。

カスタムフック(Custom Hooks)

Reactの機能を活用する関数をフックと見なすことができる。

前で似たようなことを言った。

フックが関数と似ているなら、Reactが提供する基本機能以外にも私たちが直接実装できるのではないだろうか?

Hook calls, they are just function calls.
> REACT CONF 2018

実際、REACT CONF 2018で講演者がフックを公開しながら言った言葉だ。

JSX

上記のようにReactコンポーネントの内部ロジックを分離すること、特にフックを分離することをカスタムフックという。

このように分離することで、コードを関数で分離するのと同様にコンポーネントの可読性を高めるだけでなく、再利用性を高めることができる。

特に、関心事の分離(separation of concerns)がより明確に行われることが利点だ。

カスタムフックは必ずuseで始まらなければならない。

これには2つの理由がある。

  1. 自動的にフックが誤って使用される場合を検査するためのLint活用のためである。
  2. コンポーネント内のコードで関数との混同を避け、これがフックであることを明確に知らせるためである。

カスタムフックは場合によってReactの基本フックを活用する関数である。

これにより、他の開発者にここにuseStateuseEffectのようなReactのフックを使用していることを明確に知らせるためである。

フック(hooks)の規則で言及した規則を守るのに混乱を招かないためである。

最終比較コード

JSX

コード行は似ているが、関心事が明確に区分されているのが感じられるだろうか?

フックを他の場所でも使用できる再利用性も備えているし。

まとめ

これまでReactのフックについて見てきた。

React公式ドキュメントでも関数コンポーネントとフックを使用することがより推奨されている。

それでもLegacyを保証するためにクラスコンポーネントは引き続きサポートされているので参考にするといいだろう。

React公式ドキュメント - クラスを廃止しないという内容
React公式ドキュメント - クラスを廃止しないという内容

最後に、これまでの内容をまとめると次のようになる。

特徴クラスコンポーネント関数コンポーネント (Hooks使用)
状態管理this.statethis.setStateを使用して状態管理useStateフックを使用して状態管理
ライフサイクルメソッドcomponentDidMountcomponentDidUpdatecomponentWillUnmountなど様々なライフサイクルメソッド使用useEffectフック一つですべてのライフサイクル管理
コードの簡潔さクラス宣言、コンストラクタ、バインディングなどでコードがやや複雑簡潔な関数宣言でコードがより綺麗で理解しやすい
再利用性状態とロジックを再利用するのが難しく、高階コンポーネント(HOC)やレンダープロップを使用しなければならないカスタムフックを通じてロジックと状態を簡単に再利用可能
可読性thisキーワード使用と複雑な構造で可読性が落ちる可能性がある明確で直感的な構造で可読性が向上
複雑度クラス内部の複数のメソッドと状態管理でコンポーネントが複雑になる可能性がある状態と効果を別々のフックに分離してコンポーネントが単純になる
関心事の分離状態管理とUIロジックが同じクラス内に混在しているフックを使用して状態管理、サイドエフェクト、コンテキストなどを明確に分離
thisバインディング問題イベントハンドラでthisバインディングが必要thisを使用しないためバインディング問題なし
性能最適化shouldComponentUpdateなど別のメソッドを通じて最適化が必要React.memouseMemouseCallbackなどで簡単に最適化可能
テストの容易さクラスメソッドの複雑さでテストがやや難しい可能性がある純粋関数形式で作成されてテストが容易
将来性現在は関数コンポーネントとフックが主流なのでクラス型は徐々に使用が減少最新React開発パターンに適合し、継続的なアップデートとサポートを受ける