やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【作って学ぶフロントエンド】ReactのuseStateの仕組みについて

Hooksについて

Reactのバージョン16.8に導入されたHooksという機能によって、class componentを使用せずとも、functional component(以降: FCと記述)にstateを用意することが可能になりました。

FCstateを用意するのは非常に簡単で以下の構文を記述するのみです。(公式のサンプルから引用)

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>
  );
}

reactjs.org

useStateの戻りは配列で2つの要素を持っています。引数にはstateとして記録したい値の初期値を渡して上げれば良いようです。上記のサンプルでは0という値をセットしています。

useState(0);

userStateの戻り値は2つの要素を持つ配列です。1つ目の要素は実際にstateに記録した値です。上記のコードでは0countという変数に保持されています。そして2つ目の値が後に詳しく解説しますが、closureと呼ばれるstateを更新するための関数です。この関数に次のstateとして記録したい値を引数に渡して関数を実行することでstateの更新がされます。

...

当たり前のようにstateが用意出来て、更新も出来てしまっていますが、どんな仕組みになっているか気になりませんか?
どのようにFCが内部にstateを記録しているかという話を実際にコードを記述して解説しようと思います。コード量の少なさにきっと驚くことでしょう。
この仕組みを理解するために、まずは関数型プログラミングについて簡単に紹介します。

(このツイートが好評だったのでこの記事を書きました)

関数型プログラミングで出来ること

関数型プログラミングの全体像を説明すると非常に長くなってしまうので、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関数の機能です。filterreduceなども同様に引数に関数を受け取ります。

関数の戻り値に関数を返す

カリー化と呼ばれる(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つの「関数の戻り値に関数を返す」中で用意したadd10add20が内部にそれぞれ、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について触れている過去記事もありますので、合わせてご参照下さい。

www.okb-shelf.work

www.okb-shelf.work

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について記述した記事ですが私自身、関数型言語を好んで使っていて、そのデザインに日々、感動しています。
少しでも「関数型プログラミング凄そう」と思って頂けたのであれば、以下の記事も覗いて見てください。

www.okb-shelf.work

www.okb-shelf.work

参考文献