recomposeのwithStateとwithHandlersをコードリーディングする

前回の記事の続きです。
recomposeでよく使われるAPIである、「withState」と「withHandlers」の内部のコードを読んでいきます。

recomposeってなに?って方は、以下の記事も参考にしてみてください。
【参考】ReactNativeでHoCとRecomposeを使う | mrsekutの備忘録

これらはどんなAPIか

最初にwithStateとwithHandlersについて、簡単に説明しておきます。
前者は、SFCにローカルなstateを定義するHoCで、後者はそのstateに対して変化を加える関数を定義するHoCです。
こんな雑な説明では全然わからないと思うので、公式のドキュメントを参照してください。

コードリーディング

recomposeの一つ一つのAPIはとても小さく、それぞれ独立したものなので頑張ればよめるもの(のはず)です。

withStateを読み解く

コードはこちら

た、たったの45行ですね!!!!
知らない文法があると詰むので、最初に列挙してみます。

  • reactのcreateFactory場所
  • [stateName]: hogehogeのとこ→場所
  • 入れ子ののアロー関数→場所

くらいですかね。
最初にこの3つを調べてみます。

createFactoryとは

【参考】React Top-Level API – React

createFactoryは、普段は全く使いませんが、Reactが用意しているAPIの一つです。
JSXを使わない素のReactですね。
指定したtypeのReact.Elementを生成する関数を返します。
試しにちょっと使ってみます。

これを適用すると、以下のようなDOMが生成されます。
<div class="hello">hello world</div>

同様にして、以下のようにしてしてあげればpropsも渡すことができます。
同じく親コンポーネントの一部です。

子コンポーネント。

これで親から渡された中身が「hello world」のtestという名のpropsが渡されます。

これでなんとなくcreateFactoryの挙動が把握できました。

[stateName]とは

ES6から導入された記法のようです。
以下の記事にとてもわかりやすく書いてありましたので、そちらを参考にしてみてください。

【参考】ES2015以降のJavaScriptでObjectのkeyに変数を使う – Qiita

要はObjectのkeyを変数で指定してあげることができるようになったみたいです。

二重アロー関数

まず最初にこんな感じのアロー関数ってどんな挙動を示すかしっかり理解できていますか?
僕はできていませんでした。

アロー関数が入れ子になっています。
これはJavaScriptでカリー化した関数が定義されています。
カリー化は関数型プログラミングでよく出てくる概念です。

この関数を実行するためには、以下のようにします。

これだけでは、何が良いのかわかりませんが、カリー化された関数の効能を知るためにはこんな感じでうことができます。

このように引数を固定した関数を新たに宣言することができます。
これを「部分適用」といいます。
また、これは引数とカリー化される順番も重要です。
calc2()の引数はyじゃなくてxであることに注意です。
これがあとから効いてきます。

じゃあ、次にこんなのはどうでしょう。
さっきと違って2つ目の引数を取らないカリー化された関数です。

試しに、add2(3)と実行してみると、

というのが返ってきます。
つまり、さきほど同様、値ではなく関数が返ってきます。

型を調べてみると、この通り。

なので、値を返すためには、こんなふうに実行する必要があります。

じゃあ例えば、

とするとどうなるか。

これは、2つ目の引数には関係なく5が返ってきます。

だから、同じようにこの関数に対して部分適用した関数を作ると、このabc()という関数は何を引数にとったとしても5を返す関数になります。

【参考】reactjs – What do multiple arrow functions mean in javascript? – Stack Overflow

レッツコードリーディング

さて、前置きが長くなりましたが、コードを読んでいきます。
まずは大枠から見ていきます。
このwithState()関数をめちゃくちゃシンプルにするとこんな感じになります。

まさにHoCといった形になっていますね。
HoCに関してはこちらの記事などを参考にしてみてください。
渡されたコンポーネントに少し肉付けしたコンポーネントを返すコンポーネントの形になっています。

まずこの部分から見ていきます。

withStateの3つ目の引数の型がfunctionかどうかで、内部stateの値をセットしています。
関数ならpropsを渡した関数を、そうでないないなら渡ってきたinitialState自体をセットします。

次にこの部分。ここが少し厄介。

callback関数は一旦無視して書き換えてみましょう。
ほら、少しシンプルになりました。

witshStateの第2引数で渡した文字列がここで定義されるものの関数名、もしくは変数名になります。
コレを見てわかるのは、関数の内部はここで定義されていないということです。
ここでは、先ほど定義したstateValueというstateに関数もしくは変数をsetStateしているだけですね。

落ち着いて一つずつ見ると単純なものの組み合わせだということがわかってきました。

withHandlersを読み解く

では、次にwithHandlers()を内部コードを見ていきます。
コードはこちら

同じように進めていきます。
ここで、新しく出てくるものにmapValues()関数があります。
定義はこのページにあります。

この関数は、objectとfuncionを引数にとり、
そのobjectのキーに対して、そのobjectの値とキーをfunctionの仮引数とした関数を値とした、辞書を返します。
つまりこんな感じの辞書を返します。

{objectのキー: function(objectのvalue, objectのキー)}

レッツコードリーディング

では、withHandlersの中身をを見ていきます。

mapValues()の第1引数に以下のオブジェクトを渡しています。
typeof handlers === 'function' ? handlers(this.props) : handlers

同様にして、mapValues()の第2引数に以下のfunctionを渡しています。

なんかよくわからないので、この関数にhogehogeという名前を付けてみましょう。

先ほどのカリー化の関数に似ていることがわかります。
これを少し簡略化して、書き換えてみると、

ふむふむ。だいぶわかってきました。

引数にとった関数にthis.porps...argsを渡しています。
この...argsに先ほど見たvalとkeyが入ります。

ここで前回の実装内のコードのwithHandlersを使っている部分を見てみます。

ここでは、BaseComponentはまだ出てきておらず、中の3行は全てhandlersに当たります。
この3行全体がmapValuesに渡されるobjectになります。
例えば1行目なら、incrementがkeyで、({ updateCounter }) => () => updateCounter(counter => counter + 1)がvalueになります。

で、このmapValuesの出力を見てみると、先ほどにも説明した通り、keyとfunctionのオブジェクトが返っていることがわかります。
今回のケースでは、以下のようなイメージのオブジェクトが返ってきます。

ここで、このオブジェクトのincrementのvalの中身を見てみると、

こんな感じになっています。

で、このcreateHandler()とは一体なんぞやというと、さきほどの({ updateCounter }) => () => updateCounter(counter => counter + 1)になります。
つまりcreateHandler()は3つ以上の引数をとるカリー化された関数である必要があるということがわかります。

この関数に「incrementFunc」と名前を付けて冷静に見てみます。

そもそも、これがどんな関数かというと、先ほど見たような、「2つ目の引数に何も取らないカリー化した関数」です。
それで、このupdateCounterに当たるのが、withState内のupdateFnになります。

なので、実装時に気にする部分はupdateCounter()の引数に当たるcounter => counter + 1のところですね。
この、counter => counter + 1という関数の戻り値がwithStateのstateValueにあたります。

イメージはこんな感じです。

ということで、withHandlers内で定義した関数を、コンポーネント内で実際に使うときに、引数をとりたいのであれば、以下の「ここ1」の部分で引数に渡せば良いことがわかります。

これをアロー関数になおすと、以下のようになります。
increment: ({ updateCounter }) => (hoge) => updateCounter(hoge)

これは、さきほどの前回の実装内のコードのwithHandlersを使っている部分のresetの部分で使っています。

といったように、ラップされたコンポーネント内ではこんな感じに引数を与えることができます。

所感

長くなりましたが、これでこの2つのAPIの内部がなんとなく読めるようになってきました。
コードを読みながら、HoCを最初に考えた人すげえな・・って思いました。

コメントを残す