ハンバーガー屋で隣の女子高生が語るReact 第3話 Hooks
ハンバーガー屋さんで隣の女子高生がReact Hooksについて語っていました。
・ 第一話 React
・ 第二話 隣の女子高生が語るデータの持ち方
・ 予定 React Hooks チューニング
・ 予定 React 自動テスト
このシリーズを加筆・修正して、働きアップグレードガイド2020 〜楽しく働くために取り組めること〜という合同誌に収録することが決まりました。この合同誌、フルリモート・フルフレックスの会社でどうやって楽しく働くのか?2020年代の働き方についててんこ盛りの本なので、興味ある人はぜひチェックリストに追加してみてください。
データの流れが明確化したことで訪れた変化
J美「さっき、Reactはもっとシンプルになってるって言ってたけど、どんな風にシンプルになったん?」
R子「元々Reactが生まれたときって、Reactコンポーネントは自前の状態(ステート)を持ちつつ、ツリーの根っこから渡されるハンドラによって、自分の状態更新を自分の親やそのさらに親に伝えとってんよー。ただ、そんときは当然、MVVMとかと変わらん複雑さがあったわけやな ー」
Reactは元々、コンポーネントに渡された引数によって仮想DOMを操作し、渡されたハンドラによって状態を更新するという考えで作られていて、それがまさにリアクティブプログラミングだったためにReactという名称がつけらている
リアクティブプログラミングはたとえば、Excelなんかがそうだが、あるセルの値が書き換わると、それに応じて他のセルも変更される。Reactでも、あるコンポーネントの引数やイベントによって他の状態も変化するという構造である。
J美「Flux/Reduxがデータの交通整理を他が引き受けてくれたから、Reactの構造自体がそこまでややこしいことをしなくて済むようになったってこと?」
R子「せやねー。Reactが当初考えてたよりもシンプルにやってもいいってみんな気づいたんよー。最初のReactはクラス型コンポーネントで複雑だけど高機能やったけど、そういう理由で、途中から関数型コンポーネントっていう、純粋関数を使うようになったんやねー」
クラス型コンポーネントは、昔のReactコンポーネントの作成方法で、React.Component を継承したクラスのこと。
関数型コンポーネントは、単一の関数がコンポーネントになるというもの。
J美「さっきから純粋関数ってよく出てくるけど、なんで純粋関数が出てくんの?そもそも純粋ってなんなの?ピュアなん?」
R子「純粋関数って、副作用とかが無くて、入力によって出力が確定する関数のことやねんけど、これの利点は仕様が分かりやすいことと、ユニットテストがめっちゃ書きやすいことやねー。特にReactの純粋関数コンポーネントやったら、引数を JSX のツリー構造に変換するだけのプログラムになるから、デバッグもテストも開発も簡単になるんよー」
純粋関数。関数型言語の人たちが好むやつ。Reduxなんかも実は制約として、引数を書き換えちゃいけないというルールがあったりする。
副作用はプログラミング中級者以上を目指すなら、覚えておくべき概念。I/Oや、配列やオブジェクトの中身の書き換え、変数の書き換えなどはすべて副作用。ちなみに純粋関数でI/Oを行うためには、ちょっとした概念の導入が必要になるが、関数型言語を布教する記事ではないため省略する。
ユニットテストでは、副作用は極めて厄介な存在である。対処方法としてはモックを使うなど。
R子「ただなー。Flux/Redux + 純粋関数だとどうしても問題があってなー。仰々しすぎて、どうせならコンポーネント自体が状態を持つ方が好ましいときってのがあんねんよー」
J美「Inputコンポーネントで使うような現在入力中の文字列とかって、ストアで持つのはさすがにアホらしいもんね?」
R子「そうそう。Flux/Reduxは素晴らしい革命的なヤツやったけど、なんでもかんでもストアにデータ貯めるのって、それはそれでアンチパターンやねんよー」
アンチパターン。みんながハマりがちなパターンのこと。Reduxなんかはシングルストア・シングルソースという概念を持つため、ある一つの場所に、細々した情報すらため込まれるため、ぶっちゃけ、無駄に仰々しい。Reactが難しいって感じたら、だいたいReduxアンチパターンのせいだと思っていい。
J美「それどうやって解決すんの?今までのReactやFlux/Reduxの仕組みじゃどうしようもなさそうだし、せっかくFlux/Reduxで簡単になったものが逆戻りしちゃいそうなんだけど」
R子「そこで一時期流行ったんが、Recompose/HoCっていう考え方やねんなー。JavaScriptって元々関数型言語の流れをくむ言語っていうのもあって、高階関数を合成することで、純粋関数コンポーネントに、状態を持つ機能を後付けする仕組みやね」
高階関数自体はJavaScriptだと当たり前のように使われている。関数やメソッドの引数に関数を指定するというもの。これはJSでは最初期からそうなっていたが、古い言語の中には関数を自由にやり取りできないものもあった。
高階関数の合成、要するに引数として受け取った関数と別の関数を足し合わせて、ある純粋関数に別の機能をアドインするようなもの。純粋関数自体は綺麗なままで、状態保持や副作用を合成する別の関数に押し付けたりできる。
J美「じゃぁ、React + Redux + 非同期ミドルウェア + Recompose / HoC っていう組み合わせが流行ってたの?」
R子「そうそう。一部の意識高い系React民に好まれてたんやわー」
J美「でも、それってただでさえ複雑になってたReact + Redux が、さらにややこしくなるってことよね?」
R子「せやねー。結局のところ、純粋関数は便利だけど、機能追加をRecompose/ HoC のやり方でやるのは大変やから、React自体に大きくメスを入れたんよー。それがReact Hooksっていう、Reactの関数型コンポーネントに、状態とか副作用をもたせつつも、純粋関数のときとほぼ同様のメリットを享受できる仕組みやねん」
React + Redux + 非同期ミドルウェア + Recompose / HoC は正確にはコンポーネントは全部純粋関数で書いたうえで、Recompose というライブラリによって、機能を合成していた。
React Hooksは、React 自体が Recompose のような機能を React 内部をいじることで実現したもの。そりゃ本家本元が React 自体いじる方が確実だ。
J美「React HooksとRecomposeでシェア争いみたいにはならなかったの?」
R子「それがなー。Recomposeの作者もReduxの作者もFacebooks社に入社して React Hooksの開発者側に回ったから、シェア争い以前に趨勢が決まったんよー。Facebook社もうまいこと立ち回ったもんやわー」
ちなみにRecomposeの開発はもうされないらしい。そりゃまぁそうだろう。
J美「それはそれとしてReact Hooksはどういう機能持ってるん?」
R子「React HooksはHooks関数っていう、特定の条件でのみ使える特殊な関数を提供しとってなー。具体的には関数型コンポーネントかつ、その関数のトップレベルであることやな。あとHooks関数はかならず use から始まる」
J美「トップレベルって??」
R子「if/for文とコールバック関数以外の場所かな。制御構造の内側は禁止されてるってことやね」
Hooks関数は、R子が言ったように、特定条件でのみ使える関数で、かつ、useという名前から始まる関数。
トップレベルと言っても、ソースコード自体のとトップレベルではなくて、関数でのトップレベルのこと。一段でも何かしらの制御構造などでネストされるとだめ。
コールバック関数は、JavaScriptだと空気のごとく度々登場するやつ。ある関数を実行するときに、処理が終わったとき、他のイベントが生じたとき、エラーが生じたときなどに呼び出される。
J美「だめなパターンいくつか教えてよ」
R子「せやな。こういうのは実際のコードで見たほうがわかりやすいなー」
import React, { useEffect } from 'react'
const Component = props => {
useEffect(() => {
console.log('これはOK')
}, [])
if (props.hoge) {
useEffect(() => {
console.log('これはNG 1')
}, [])
}
for (const line of props.lines) {
useEffect(() => {
console.log('これはNG 2')
}, [])
}
props.line.forEach(line => {
useEffect(() => {
console.log('これはNG 3')
}, [])
})
return <div>J美大好き!</div>
}
R子「NG 1はif文の中やねー。もちろんelseの中でもあかんよー」
R子「NG 2はfor文の中やねー。もちろんwhileの中でもあかんよー」
R子「NG 3はコールバックの中やねー。コールバック関数の中は結構、ナチュラルにhooks関数を書きそうになる罠やでー」
J美「これって結構制約キツくない?」
R子「まー、実際考え方をスイッチせんといかんやろねー」
JavaScriptに限らずだが、ブロックスコープの外側の変数は、クロージャーという概念の元アクセス可能になるため、そういった概念を活用する必要がある。
J美「なんでhooks関数って、こんなに条件厳しいの?」
R子「hooks関数って、Reactコンポーネントに依存した仕組みやねんけど、普通のJavaScriptプログラミングやと、ある純粋な関数が、そのコンテキストに応じて処理内容変えるってできへんやろ?」
J美「Scalaだったら implicit 引数でコンテキスト渡したりするよね」
R子「せやなー。いうてもScalaやないから、JavaScript/TypeScriptでどうにかせんといかんねん。そしたらReact自体のコンポーネント階層の情報を使って、関数にコンテキストを注入するやり方になるわけやなー」
J美「なるほど、技術的な理由ってことね」
Scalaのimplicit引数は、筆者は個人的にはかなり嫌いな存在なのだが、便利な側面もある。わざわざ関数の引数に、変数を指定しなくても暗黙的に引き回されるといった便利機能である。ややこしい操作を意識しなくても勝手に必要な情報が渡っている。implicit引数が無い多くの言語ではたとえば context みたいな引数を渡すこともある。こういったことは設計のトレードオフとして捉えられることが多い。
Reactというライブラリから見れば、コンポーネント関数はツリー構造で管理されていて、それぞれのコンポーネント関数はどういうコンテキストを持っているか?というのを知っている。コンテキストはたとえば、自分の親はどういうコンポーネントか?今保持してるステートというようなもの。クラスとインスタンスの関係に似ている。コンポーネント関数はコンテキストと合わさって、ある1つのインスタンスのように扱われる。
ここに制御構造が合わさるとコンポーネントツリーだけではコンテキストを管理できなくなってしまうため、技術的に極めて面倒な話になるから、制御構造を入れたくないというのはある。
R子「じつはそれだけやないんやけどねー。トップレベルに限定されるのは、制御構造を入れて複雑にしたくなかったっていう意図もあんねんよー」
J美「というと?」
R子「制御構造を許したら、人類には難しすぎるものになっちゃうからやねー。React Hooksプログラミングでは、関数型コンポーネントのトップレベルでやることって3種類だけやねんよー。元からの目的であるJSXを返すこと、データを受け取ること、コールバック関数の登録やねー。こうすることで考え方をシンプルにして、人類でも簡単に扱えるようにしたんやねー」
純粋関数以外の側面を hooks に押し付けた形なのだが、たとえば副作用なんかはコールバック関数の中で実行してもいいというふうにしておけば良い。
先程も述べたが純粋関数には、ユニットテストが書きやすいなどの利点がある。登録されたコールバック関数のテストを考えずにテストが可能になる。
J美「もし副作用を関数型コンポーネントのトップレベルに書いたらどうなるん?」
R子「副作用を検知する仕組み自体は無いから、その副作用次第やねー。console.log だけなら、console.log でコンソールがにぎやかになるだけかもしれんねー。ただそれで重くなることもあるかもしれへんよー。あと、下手なことやると無限ループが簡単に生じたりするでー」
J美「無限ループ?どういう場合に生じるの?」
R子「一番わかり易いのは、ステート更新をトップレベルでやっちゃうことやな。ステート更新って、関数を実行しなおして新しい状態に更新するっていうフローやから、自分で自分を更新し続けるねんよー」
詳しいことはまた次の話にしたいと思うが、基本的にReact Hooksの関数型コンポーネントのトップレベルでは、余計なことをしない。最小限の変換のみを行うというのが鉄則となる。
J美「ところでhooks関数ってどんなのがあるん?」
R子「よく使うのは、useState, useEffect, useCallback かなー。あとは状況によって useLayoutEffect, useMemo, useRef, useContext を引き出しに入れておけばほとんどケースは対処できるよー」
詳しくは、実際に、公式のAPIリファレンスを見てほしい。ただ、全部を使うことはそうそうない。
J美「関数名からなんとなく想像できるけど、useStateは、ステート、つまり状態を持つ関数で、useEffect は side effect(副作用)を実現するもの、useCallback はコールバック関数作るためのものってことで合ってる?」
R子「合ってる合ってるよー。クラス型コンポーネントのときも、propsとstateがあって、stateはコンポーネントローカルの状態のことだったけど、hooksでもその点は同じ。アコーディオンコンポーネント作ったとして、アコーディオンの状態を自分以外にもたせてもしゃーないから、自分自身でステートとして持つってことやねー。」
R子「大体React Hooksでコンポーネント作ってると、色々な処理が useEffect の中に入ることになるねー。API呼び出しをuseEffectの中に書いて、結果を取得したらその結果で、ステート更新したりする感じー。」
J美「ところで、コールバック関数は分かるんだけど、わからんない。なんでコールバック関数作るためのhooks関数がわざわざ用意されてるん?」
R子「useCallbackがあるのって、どちらかというとパフォーマンスチューニングが理由やねん。これはまた今度詳しく話すわー」
予告
・ 第一話 React
・ 第二話 隣の女子高生が語るデータの持ち方
・ 予定 React Hooks チューニング
・ 予定 React 自動テスト
このシリーズを加筆・修正して、働きアップグレードガイド2020 〜楽しく働くために取り組めること〜という合同誌に収録することが決まりました。この合同誌、フルリモート・フルフレックスの会社でどうやって楽しく働くのか?2020年代の働き方についててんこ盛りの本なので、興味ある人はぜひチェックリストに追加してみてください。
ちなみに今回のは特にそうだけど、説明が足りてない気がしてるので、分かりづらいところとかあればツッコミなどいただけると助かります!
この記事が気に入ったらサポートをしてみませんか?