Hooksについて
React
のバージョン16.8
に導入されたHooks
という機能によって、class component
を使用せずとも、functional component
(以降: FCと記述)にstate
を用意することが可能になりました。
FC
にstate
を用意するのは非常に簡単で以下の構文を記述するのみです。(公式のサンプルから引用)
import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
useState
の戻りは配列で2つの要素を持っています。引数にはstate
として記録したい値の初期値を渡して上げれば良いようです。上記のサンプルでは0
という値をセットしています。
useState(0);
userState
の戻り値は2つの要素を持つ配列です。1つ目の要素は実際にstate
に記録した値です。上記のコードでは0
がcount
という変数に保持されています。そして2つ目の値が後に詳しく解説しますが、closure
と呼ばれるstate
を更新するための関数です。この関数に次のstate
として記録したい値を引数に渡して関数を実行することでstate
の更新がされます。
...
当たり前のようにstate
が用意出来て、更新も出来てしまっていますが、どんな仕組みになっているか気になりませんか?
どのようにFC
が内部にstate
を記録しているかという話を実際にコードを記述して解説しようと思います。コード量の少なさにきっと驚くことでしょう。
この仕組みを理解するために、まずは関数型プログラミング
について簡単に紹介します。
(このツイートが好評だったのでこの記事を書きました)
React+ReduxなりReactHooks完全に理解した。
— OKB (@sing_mascle69) 2020年5月21日
これclosureでlocalなscopeで値を回してるだけってことか。そのために高階関数用意してるって考えるとスッキリ。こんな所でElixirの知見が役に立つとは思いもしなかったというマインドになっているけど合ってるんかな。
そーなんですよねー、
— YOSUKE@プログラミング ElixirとPhoenix (@YOSUKENAKAO) 2020年5月21日
高校生にReactを教えてたのを、Elixir先に教えてからReactを教えると
挫折者が少なかったのを思い出す。 https://t.co/i0YmPqTc5N
関数型プログラミングで出来ること
関数型プログラミングの全体像を説明すると非常に長くなってしまうので、useState
を理解するために必要な最低限の概念のみをご紹介します。関数型プログラミングでは以下の操作が可能になります。
変数に関数を代入する
// 変数への関数の代入 const double = x => { return x * 2; }; // 変数には関数が代入されている console.log(typeof(double)); // function console.log(double(10)); // 20
関数の引数に関数を渡す
// 関数を引数に渡す const lst = [1, 2, 3, 4, 5]; const apply = lst.map(num => num * 2); console.log(apply); // [2, 4, 6, 8, 10]
map
関数の第1引数に渡されているのはnum => num * 2
という無名関数です。この引数に渡した関数を配列の全ての要素に適合(apply)させるのが、map
関数の機能です。filter
やreduce
なども同様に引数に関数を受け取ります。
関数の戻り値に関数を返す
カリー化
と呼ばれる(Haskell Curry
に敬意を表して命名された)方法を使って、足し合わせる値を固定して状態の関数(ここでは10)を返しています。こうすることで1つの関数から複数の機能を持つ関数を作成することが出来ます。
// 関数を戻り値として返す const addN = N => num => num + N; const add10 = addN(10); console.log(typeof(add10)); // function console.log(add10(3)); // 13 const add20 = addN(20); console.log(add20(3)); // 23
うんうん、それで?
お気づきになりましたか。先ほどのサンプルコードの3つの「関数の戻り値に関数を返す」中で用意したadd10
とadd20
が内部にそれぞれ、10
, 20
という値を保持しているということに。10
, 20
の値はそれぞれ、addN
を呼び出した初回実行時にのみ、代入した値です。それ以降は一度も引数経由で受け渡したり、環境変数などで更新は行っていません。
これがuseState
が内部で値(つまりはstate
)を保持することが可能となる仕組みです。一度、この仕組みを利用してuseState
に初期値を受け渡す部分までを記述してみましょう。
useState version1.0の作成
まずは受け渡した値を内部のstate
に記録する部分までを記述しました。
const useState = init => { const state = init; return state; } const count = useState(0); console.log(count);
しかしながら、この方法では内部に値を保持しておくことが出来ずに、useState
を実行するたびに、別の値が用意されてしまいます。const count
で用意した値(useState
内のconst state
)に対してアクセスする方法がないということを意味しています。
内部のstate
にアクセスするための仕組みを用意します。先ほどの戻り値に関数を使用するという方法を使ってみましょう。
const useState = init => { const state = init; return () => { return state; } } const count = useState(0); console.log(typeof(count)); // function console.log(count()); // 0 console.log(count()); // 0
この方法ではuseState
の戻り値の関数を使うことで、useState
の内部に記録されているstate
の値を覗き見(重要なのであえて覗き見という表現を採用)をすることが出来るようになりました。何回でも実行出来て、useState
内部のstate
が更新されることはありません。安心ですね。鍵のない取り出し不可能な金庫のようです。
...
しかし、困りました。本家useState
は戻り値の第2要素の関数を実行することで内部のstate
を更新することが出来ました。次は、更新するための仕組みを用意していきましょう。
closureについて
一言で説明すれば、関数内に用意されている値を取得、更新するための専用の関数のことです。先ほど実装したuseState
の戻り値が実はclosure
になっています。
const useState = init => { const state = init; return () => { return state; } }
これは値を取得するためのclosure
です。値を取得する事は出来ますが、更新することは絶対に出来ません。絶対に。
今の自分たちに必要なのは関数内に用意した値(state
)を更新するためのclosure
です。
closure
について触れている過去記事もありますので、合わせてご参照下さい。
useState version2.0の作成
内部の値を更新するために値(state
)を更新する関数を返すようにします。
const useState = init => { let state = init; return (val) => { state = val; return state; }; } const count = useState(0); console.log(typeof(count)); // function console.log(count(1)); // 1 console.log(count(2)); // 2
まず内部のstate
の宣言を値の更新が行われるため、const
からlet
に変更しました。合わせて、戻り値の関数は引数経由で更新したい値を受け取り、内部のstate
を更新するようにします。この妙義がなせるのはreturn
で返している関数からはlet state
に対してスコープが有効であるためです。逆に言えば、戻り値として受け取れる関数以外ではstate
の値を更新することが出来ないということです。
これでほぼ全ての機能の実装が完了したので、本家useState
に従って戻り値を配列にしておきます。
const useState = init => { let state = init; return [ state, (val) => { state = val; return state; } ]; }; const [count, setCount] = useState(0); console.log(count); // 0 console.log(typeof(setCount)); // function console.log(setCount(1)); // 1 console.log(setCount(2)); // 2 console.log(setCount(3)); // 3 console.log(count); // 0
いい感じですね。本家useState
を再現出来ました!!
最後に
たったこれだけのコードで高機能な関数を用意できるのは関数型プログラミング
の成せる技です。useState
について記述した記事ですが私自身、関数型言語を好んで使っていて、そのデザインに日々、感動しています。
少しでも「関数型プログラミング凄そう」と思って頂けたのであれば、以下の記事も覗いて見てください。