Edited at

おまえはReact hooksを知っているか

よく来たな。おれは毎日すごい量のコードを書いているが、誰にも読ませる気はない。しかし今回はReact hooks という真の男のためのAPIを発見したのでいてもたってもいられずQIITAに記事を書くことにした。

(この記事の文体は、逆噴射聡一郎先生のパロディです。)


おまえはReact hooksを知っているか

お前は毎日VUEだとかREACTだとかPWAだとかBBBFFだとかそういう流行に常に振り回されながらフロントエンドというメキシコを生きている。フロントエンドで生まれてくる技術のほとんどは、マッチの火より儚くすぐ消えてなくなるものだ。しかし、流行に乗り遅れるのを恐れているおまえはそういった技術にとびつき、チュートリアルをよみ、すべてを理解したと息巻いてプロダクトに導入し・・・やがてそれの流行がおわり・・・メンテをするのが辛くなり・・・しぬ。フロントエンド界隈ではへなちょこな技術がもてはやされ、しばらく経ってそれが全く使い物にならないとわかり、また別のライブラリとか変なビルドツールが流行るということが往々にしてあるが、中にはPURE GOLDOのような真の男のための技術がある。それがReact Hooksだ。


React Hooksとはなにか

React HooksはReactのFunctional ComponentでStateや副作用を使えるようにすることを目的としたAPIだ。従来Class ComponentやクソったれなHoCでしかできなかったことを可能にする。

const Counter = () => {

const [ count, updateCount ] = useState(0);
return <div>{count}</div>;
}

これはhooksの一つであるuseStateだ。このコードにはupdateCountを呼び出す箇所がないが、呼び出すとcountが更新されCounterのエレメントが再描画される。しかし、真の男をめざすフロントエンドエンジニアのおまえは思うだろう「thisも引数もなしにどうやってこんな機能を実現していんだ?できるはずがない!!これはペテンです!!」しかしこのコードは実際に動く。


仕組みをしれ

hooksに限らないがどんなライブラリでも基本的な仕組みを知ることは大事だ。async-awaitとGeneratorの関係とか、reducerの仕組みとか・・・。べつにゼロから実装できる必要はない。しかしその仕組みのESSENSUだけは抑えておく必要がある。そうするとお前は基本的な傾向・・・どういうことをすると遅くなるとか、なんでそういう制約があるかとかそういうのの理解がすごく早くなる。だから俺はhooksの仕組みを説明する。

おれはもったいぶって説明したりするのが苦手だから、即座に答えを書く。Hooksは裏で配列をもち、それを順番に呼び出している。

const hooksArray = [];

let hooksIterator;

const render = (Counter) => {
// これはreactのrenderとは別物だがイメージだ
hooksIterator = 0;
return <Counter></Counter>;
}

const Counter = () => {
const [ count, updateCount ] = useState(0);
return <div>{count}</div>;
}

const useState = (initialValue) {
const hooksIndex = hooksIterator++;

if(hooksArray[hooksIndex] == null) {
// 初期値を設定する
hooksArray[hooksIndex] = initialValue;
}

return [
hooksArray[hooksIndex],
(nextValue) => { hooksArray[hooksIndex] = nextValue },
];
};

先に断っておくがおれはこのコードは実行してない。これが完全に正しく動く保証はないし、reactのチームはこれの100倍ぐらい速くて堅牢なコードを書いている。しかし、概念だけ理解しろ。hooksは裏で用意した配列に頭から順に値をいれていく。次のレンダリングでも同じ順番で呼び出せば、さっき入れた値が返ってくる・・・ただそれだけだ。マインクラフトしかやらない小学生でも理解できる仕組みだ。

配列に入れていくだけだから、当然呼び出す順番や回数がかわると問題が起きる。だからhooksではそうなるような行為「ifやforのブロックの中での呼び出し」を禁止している。「render以外のタイミングで呼び出す」のも当然禁止だ。しかしこれらの禁止事項は仕組みからくる当然の要求であり、べつにおまえにいじわるをしてるとかタルサドゥームの罠とかそういうのじゃない。


一番大事な関数を知れ

さて、hooksの仕組みを知ったおまえはhooksを使う権利がある。react hooksの公式のウェッブサイトにいくとuseなんとかとかいう関数が10個ぐらいあって、はやくも勉強をしようとするお前の心を折りにくるが、実際にはおまえは大事なやつだけ覚えればいい。useState, useEffect そしてuseMemoだ。

useStateは前項で説明したから省略する。

useEffectは副作用をひきおこすときに使う、EffectというのはSIDE EFFECT、つまり副作用のことだ。エフェクトというのは別にボタンが光るとか文字が流れるとかそういうVFXだけではない。Componentの返り値に影響しない効果・・・ログをながす・・グローバル変数をかきかえる・・ウェッブAPIから値を取得する・・・canvasの中身を描画する・・・タイマーを設置して時間差でイベントをおこす・・・こういうのは全てエフェクトだ。だからこういうことを起こしたいときは、必ずuseEffectのなかに書く。

useEffect(() => {

postCounterAPI(count);
}, [count]);

コード第2引数はdeps・・依存関係を記述する。配列の中身のどれか一つでも変更があったとき、この副作用は実行される。↑のコードの場合は、countの変更があったときだけ、APIにPOSTするといった具合だ。

depsは副作用を引き起こす依存関係をを宣言的に記述することができる。宣言的というのは、変更や状態が少ないということだ。動くものが少ないデバイスはタフだ。HDDよりSSDのほうが壊れにくいように・・・おまえがPROのエンジニアなら、プログラムが安定して動くように可動部をすくなくすることにつねに気を配る必要がある。

つぎにuseMemoだが、これはシンプルだ。

const countIsEven = useMemo(() => count % 2 == 0, [count]);

引数で与えた関数はdepsに変更があったときだけ実行され、返り値を返す。depsが同じ場合は、以前計算した結果を返す。おまえはこれをつかって教科書みたいなメモ化戦略をとることができる。

ほかのhooksはほとんどが糖衣構文みたいなもので、ほかのフックで代用できる。

たとえばuseCallbackはuseMemoで代用できる。

useCallback(() => {console.log(count)}, [count]);

// ↑は↓と同じ
useMemo(() => () => {console.log(count)}, [count]);

useCallbackをはじめとする他のAPIは、基本的なAPIが手に馴染んできたら、自分で必要なときに調べてつかえばいい。


自分だけのHookをてにいれろ

ここからが重要な部分だ。hooksはシンプルでつかいやすいAPIだがべつに同じことはClass Componentで実現できる。おまえはコンストラクタでステートを初期化し…setStateとかで変数を更新すればいい…。むしろオブジェクト思考に毒されたおまえは主語のないhooksに違和感を覚え、使うことを避けすらするだろう。しかしHooksは実際にはClass Componentより柔軟な側面を持っている。これは実例を見るのがはやい。

おまえはウェブアプリを実装しているが、ユーザーステータスを定期的にAPIからひろってきて、更新する必要がある。APIはSSEとかWebsocketとかそういうハイカラなつくりはしてないから、何秒かごとにポーリングするとしよう。Class Componentで実装するとこんな感じだろう。コードがややこしくなるからエラーハンドリングとかunmout後の処理とかは書いてない。

class UserStatus extends React.Component {

constructor(props) {
super(props);
this.state = { userStatus: 'loading' };
this.timer = null;
}

componentDidMount() {
this.timer = setInterval(() => {
fetchUserStatus().then(userStatus => {
this.setState({ userStatus });
});
}, 2000);
}

componentWillUnmount() {
clearInterval(this.timer);
}

render() {
return (
<div>{this.state.userStatus}</div>
);
}
}

これは実際うまく機能するが、問題はこの機能を使い回すときにある。Class Componentを使い回す方法としては、継承とかミックスインがあるが、いくつか問題を抱えている。また、もっと高尚なやりかたとしてHoCとかがあるが、これもややこしいし、おれやお前よりずっとこの問題について考えてきたrecomposeの作者がもうhooksで十分といっている。このへんの詳しい説明はこの記事がわかりやすい。

対するhooksはこうだ。


const UserStatus = () => {
const [ userStatus, updateUserStatus] = useState('loading');

useEffect(() => {
let timer = setInterval(() => {
fetchUserStatus().then(userStatus => {
updateUserState(userStatus);
});
}, 2000);
return () => {
clearInterval(timer);
};
}, []);

return (
<div>{userStatus}</div>
);
};

さて、この機能を使い回すときはどうするのか?答えは「そのまま括りだす」だ。

const useUserStatus = () => {

const [ userStatus, updateUserStatus] = useState('loading');

useEffect(() => {
let timer = setInterval(() => {
fetchUserStatus().then(userStatus => {
updateUserState(userStatus);
});
}, 2000);
return () => {
clearInterval(timer);
};
}, []);

return [ userStatus ];
}

const UserStatus = () => {
const [ userStatus ] = useUserStatus();

return (
<div>{userStatus}</div>
);
};

ちょっとまて?こんなやりかたで大丈夫なのか?おまえは訝しむかもしれないが、これでいい。なぜならhooksは、ただ配列に値を入れて順番に呼び出してるだけだからだ。順番さえ変わらなければ関数で括るとかしても全く問題ない。そして括った関数はuseなんとかという名前をつける(これは慣習だ)。クラスコンポーネントを使った場合と比べて、かなりすっきりしているだろう?しかも、これはただの関数だから、へんな高階コンポーネントとか生成したりしない。これは純粋なロジックの使いまわしだし、おまえはルールさえ守ればこれをどこで呼んでも何回呼んでもかまわない。

このカスタムフックは、ただ呼び出すだけで機能する。おまえはこの関数をギフハブで得意げに公開してもいいし、おまえだけのGUNとしてハードディスクにこっそりと忍ばせておいてもいい・・・。


lintを忘れるな

ここまで読んだお前は今にでもhooksを使いたくなっているだろう。しかし少し待て。hooksのルールはシンプルだが実際機械でない人間がやるとミスがたくさん起きる。ついifのなかでuseEffectを呼んだり、useMemoの依存関係を書き忘れたりする・・・。そういった少しのミスでアプリは崩壊し、開発チームはメキシコの荒野へと放り出させれ、ベイブはおまえを見捨てて家を出ていき、おまえは倒れてきたサボテンの下敷きになって死ぬ・・・。そうなる前におまえはlintを入れるべきだ。

reactは公式でeslint-plugin-react-hooksというlintプラグインを出しており、これを使うことで、さっき書いたようなミスはすべて洗い出すことができる。これは実際強力で、lintなしでhooksを使うのはまったくおすすめしない。


先人たちが残したHooksをつかえ

これだけ気軽にhooksを作れるのだから、当然既にたくさんのhooksが公開されている。

awesome-react-hooks

↑のリンクに乗っているようなhooksは、その中でも選りすぐりの真の男たちが作ったhooksだ。おまえはこれらのhooksを使って今までよりずっと早く、堅牢なウェブアプリを構築することができる。まだ足りてないhooksがあると思えば、誰よりもいち早く作って公開し、真の男を目指すこともできる。