monologue

クズエンジニアの独白

【React】カスタムフックをハンズオンで試して理解する

React のカスタムフックについて理解して現状業務でのコードをリファクタしたいと思って調べてたら良い記事を発見したので、今回は記事に沿ってハンズオンで手を動かしながら理解を深めてみる。

qiita.com

少し横道にそれるが記事内で使われていた「CodeSandbox」というサービスすごい。今どきの環境(一瞬で React x TypeScript 環境が動いた)をサクッと準備して試せる。かなり使えそうなので今後ハンズオン系で積極的に使ってみよう。という発見もあった。

codesandbox.io

ハンズオン

それでは本題。記事に沿って実装していく。ページネーションの UI パーツを題材にしている。 本記事では上記の記事を進めていった過程での気付きや感想、少し横道に逸れて基本的な概念をほぼ自分用に書いているので、途中のコードは本記事上では割愛しているので元記事を参考にどうぞ。

最終的なコード(下記のv6)は下記。

v1

各ボタンのonClickに直接処理を記述。 ページネーションのロジックでhistoryの配列を作って、currentPageは配列の最後の値を参照するというロジックは自分の中では新鮮だった。history扱う系だとメジャーなロジックなのかな?

v2

先程PageコンポーネントonClickに直接書いていたロジックをカスタムフックをして抜き出した。 PageではfirstPagelastPageのみを定義し、カスタムフックの引数にしてcurrentPageや各ボタンのonClick時の処理を受け取る。

この時点でPageコンポーネントは、View関連の実装のみになって読みやすくなっている。ローカル state は呼び出し元の Page コンポーネントに所属している。

ただこのPageコンポーネントの状態だとuseLocalHistoryを利用する前提の構成になっているから結局依存している気もする。コードの見通しはViewとロジックが分離されて良くなった印象はある。

v3

onClickで実行する各関数を一つのオブジェクトに集約しつつインターフェースを定義。結果、明確に操作系のロジックを集約し、型チェックが行われるようになった。集約したことによりコンポーネントへの引渡しが容易にもなった。

インターフェースは TypeScript の機能。イマイチどうゆう役割なのかきちんと理解してないので軽く調べてみた。 インターフェースを使う最大のメリットは、「必須のメンバがーが含まれることやメンバーの型を保証してくれる点」と言えそう。また、関数の引数がオブジェクトの際に型を定義するときに使うのは書式的に便利。これは実際よく使っている。

qiita.com

note.com

v4

onClickでの操作で共通のロジックがあるので、この部分を更にカスタムフックとして切り出す。カスタムフックは多段構成(入れ子)が可能のようでどんどん共通化できるみたい。

今回カスタムフックとして抜いたロジックは、一般的には「スタック」と呼ばれる最も基本的なデータ構造の一つらしい。流石に何となく知っているけど説明しろと言われるとおどおどするレベルのクズエンジニアなので一応復習。

スタックはLIFO(last-in-first-out)、最後に入れたものを最初に取り出すデータ構造。本を積み上げたときと同じなのでstack(積み重ねる)と呼ぶらしい。 ちなみに「データ構造」とは、データを一定の形式に格納したものを指す。

話を戻して今回はこのスタックを useStack というカスタムフックに抽出しているが何が嬉しいのか。元記事には下記記述が。

これによりuseLocalHistoryがStackの実装詳細を意識せず、画面遷移の制御だけをロジックとして持つようになりました。

useLocalHistoryは画面遷移の制御、useStackはスタックのデータ構造ということか。 言葉で表現することすら難しさを感じるが、画面遷移の制御とスタックの操作は密結合というかどちらもあって初めて成立する印象があり分離したメリットをあまり感じられてない。多分これは自分の抽象化やプログラミング設計のスキルが低いことが要因な気がする。

もう少し強引にこじつけてみると、画面遷移では「トップページの場合は移動しない」「ラストページより先に進めない」等のページ制御ならではの特有のロジックがある。また、随所にスタックのPushという同じロジックを使ったりしている。 この当たりがスタックとして分離する意味なのかな?あとはスタックを全く別の部分で使うときにも再利用できるとか? 今の自分にはこの位の解釈が限界。

また別の点で、useStackの中でuseState使ってローカルステートを定義してるが、分離したスタックのロジックを利用するだけで、ローカルstateを利用できている。今までuseState使うときは React 特有の Hooks のsetStateを意識的に使っていたけど、普通にPopとかPushとか自分で定義したロジックを使えるのは良いのかな?あと使い方が明確になっていいる点は良さそうな気がする。

あとコードで結構でてくるTは何だっけとなったので復習。Tは TypeScript の Genericsジェネリックス)という機能の文脈で使う仮引数。下記の記事がわかりやすかった。まだ理解が追いついてないので追々調べていこう。

Genericsは抽象的な型引数を使用して、実際に利用されるまで型が確定しないクラス・関数・インターフェイスを実現する為に使用されます。

qiita.com

v5

useStackは v4 でuseStateを使用しているのを v5 ではuseReducerを利用して書き換える。

reducer は Redux 等の文脈でよく聞くが、Hooks での useReducer に関しては、役割自体は Redux のものと似ているが完全に同じではなく、Hooks ではよりカジュアルにに使える印象。

useStack 関数は、v4ではstackの前の状態を利用した手続き的なコードでしたが、v5では ACTIONS_POP のように、前の状態を知らずにイベントを発火させるだけでよくなりました。またreducerも手続き的なコードを書く必要はなく、新たな状態を返すだけで目的を達成することができるようになります。

記事にはこのようにあるがあまり腑に落ちてない。むしろ少し複雑になってる気がする。この辺も一旦時が解決すると信じて一旦今回はスルー。

v6

Container と Presentational にコンポーネントを分離。 Presentational は view にのみ責務を持ち、Container はロジックを持つようにする。 一つのファイル内に書いているし、Pageは export してないことから他で使う前提になっていないけどココまでする意味は何なんだろう。 よく理解できてない部分は多いけど、ようやくハンズオンが終了。

まとめ

そしてこの記事が本当に参考になるなあと感じたのは下記の点。ここまでで「何でここはこうなるんだろう」と感じた部分はきっと下記のような原則を学ぶことで解消される日がくるのでしょう。

コンポーネントからロジックが分離 (Presentation Domain Separation)

一般的なデータ構造をロジックから分離 委譲(delegation)

history履歴がPageコンポーネントには渡らない 情報隠蔽(カプセル化)

インターフェースの定義 開放閉鎖原則(OCP)

Pageコンポーネントの関数化 副作用を内部で取得せず引数として受け取る

各モジュールの設計意図が明確化 単一責任の原則(SRP)

PDS、委譲、カプセル化、SOLID原則(OCP, SRP)などのとおり、今まで培われてきたオブジェクト指向設計と何も変わらない普遍的な設計能力が必要とされます。 React Hooks特有の設計能力が求められるものではありません。

現状の自分のレベルだと不明確な点等が多くあるけど、いつかすんなりと理解できる日がくると信じて今回はここまで。