React Hooks@React 16.7 alpha

React v16.7で関数コンポーネントに状態+αを持たすことが可能となる予定です。その仕組みがReact Hooksです。この記事ではReact Hooksの概要や背景に触れてみます。

React Hooksとは

React Hooksは関数コンポーネントに状態を持たすことができます。他にも更新用の関数・コンテキスト・ライフサイクルなどクラスにある機能同様のAPIを提供します。

クラス構文で実現できてたことが関数コンポーネントでも可能になるわけですね。

なぜReact Hooksが必要か

いくつか理由は挙げられていますが、コードの再利用性が重視され生まれた機能だと思います。クラスコンポーネントはそれ自体が状態を持つため、更に分解して他で使い回すことができません。Hooksを使うとコンポーネントのロジックを再利用可能な関数として抽出できます。

何より開発者は関数コンポーネントを好んでいます。recomposeで関数コンポーネントに状態を持たせて開発を行う人は多いでしょう。recomposeを使ったコードは再利用性が高まり、本来の意味でコンポーネントが構成部品として機能します。Hooksもこの路線だと思います。

また、現状のReactはクラスコンポーネント⇔関数コンポーネントの書き換えが容易ではない点もあります。状態が不要になったから関数コンポーネントにリファクタリングしようと思っても、一箇所を書き換えてハイ終わりというわけにはいきません(逆も同じく)。

クラスはなくなるか

クラスはサポートされ続ける。ただし、React HooksをReactでコンポーネントを定義する主要な方法にしたいとも述べられています。React Hooksが成功した場合、どの地点かでクラスはオプション(追加パッケージ)になっていくのではないでしょうか。

ちなみにHooksをサポートすることで増えるコードサイズは1.5KB程度だそうです。

Hooks API

仕様変更もあり得るため、主要な基本Hooks2つに触れてみます。

  • useState – 状態と状態更新関数のHook
  • useEffect – ライフサイクルを提供するHook

useState

状態と更新用の関数(不要なら省略可)を提供します。状態の名前と更新用の関数名をブラケットで囲み宣言します。useStateの引数は初期値です。

以下は文字列を設定していますが、オブジェクトや配列もセット可能です。ただし、変更関数に関してはsetState()と異なり、オブジェクトが丸ごと置き換わるようです。

const [name, setName] = useState('mda');

// 上記は以下と同様
const nameState = useState('mda');
const name = nameState[0];
const setName = nameState[1];

超簡単なサンプルです。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [name, setName] = useState("mda");

  return (
    <div>
      <p>User: {name}</p>
      <form>
        <input
          type="text"
          name="name"
          defaultValue={name}
          onChange={e => setName(e.target.value)}
        />
      </form>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

フォームのハンドラでsetName()を呼び出しnameに値をセットしているだけです。

ここでは使用していませんが、更新用の関数はクラスのsetState()同様に、引数に関数を指定することができます。その場合、第1引数には更新前の状態値が格納されます。

useEffect

componentDidMountcomponentDidUpdateacomponentWillUnmountの組み合わせのような感じでライフサイクルを実装できます。引数の指定で挙動が変わるので、ややこしいです。

先程のコードに以下を追加すると、アプリケーション起動時(マウント時)に、コンソールでname値が表示されます。componentDidMountと同じです。そしてフォーム入力をすると、そのたびにコンソールは最新のname値を出力します。これはcomponentDidUpdateの動きです。

function App () {
	...
	// 追加
	useEffect(() => {
    	console.log(name);
  	});
}

第2引数に空の配列を渡すと、マウント時の1回のみしか発火しません。

useEffect(() => {
   	console.log("change!"); //最初の1回のみ出力される
}, []);

第2引数に変数名を指定すると、その変数が変更されたときのみuseEffectを発火させることができます。次のデモでその動作が確認できます。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [name, setName] = useState("mda");
  const [gender, setGender] = useState("male");

  useEffect(
    () => {
      console.log("change!");
    },
    [gender]
  );

  return (
    <div>
      <p>User: {name}</p>
      <p>Gender: {gender}</p>
      <form>
        <input
          type="text"
          name="name"
          defaultValue={name}
          onChange={e => setName(e.target.value)}
        />
        <label>
          <input
            type="radio"
            name="gender"
            value="男"
            checked={gender === "male"}
            onChange={() => {
              setGender("male");
            }}
          />
          男
        </label>
        <label>
          <input
            type="radio"
            name="gender"
            value="女"
            checked={gender === "female"}
            onChange={() => {
              setGender("female");
            }}
          />
          女
        </label>
      </form>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

上記はここまでのコードに新しく性別を表すgenderという状態と対応するラジオボタンを追加しました。これをuseEffectの第2引数にセットしているので、useEffect内のコンソールログは、マウント時とgenderが変わったときのみ出力されます。nameを変更してもコンソールログは出力されません。

アンマウント時に後処理を行いたい場合は、useEffectのreturnに関数を渡します。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Message(props) {
  useEffect(() => {
    console.log("render"); // 初回と再レンダリングのたびコール

    return () => console.log("unmount"); // アンマウント時にコール
  });
  return <p>Hello</p>;
}

function App() {
  const [mounted, setMounted] = useState(true);
  const toggle = () => setMounted(!mounted);

  return (
    <>
      <button onClick={toggle}>Show/Hide</button>
      {mounted && <Message />}
    </>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Messageコンポーネントがマウント/アンマウントするたび、コンソールが出力されます。

通常はcomponentWillUnmount同様、メモリリークを避けるためにイベントリスナなどの削除に用いたりするのでしょう。

Custom Hooks

他の関数でロジックを共有するときに、関数部分をHookとして抽出することができます。記事の締めとして、Hookの切り出しを試してみます。

例えば以下のようなカウンターコンポーネントがあるとします。

import React, { useState } from "react";
import ReactDOM from "react-dom";

const Counter = (props) => {
  const [count, setCount] = useState(props.count);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(prevState => prevState + 1)}>+</button>
      <button onClick={() => setCount(prevState => prevState - 1)}>-</button>
    </div>
  );
}

const App = () => {
  return (
    <div>
      <Counter count={0} />
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

実用性はありませんが、カウンター部分をCustom Hookとして定義してみます。Custom Hook名はuseで始めるのがルールのため、ここではuseCounterとしています。

import React, { useState } from "react";
import ReactDOM from "react-dom";

const useCounter = defaultCount => {
  const [count, setCount] = useState(defaultCount);
  return {
    count, // count: count
    onIncrement: () => setCount(prevState => prevState + 1),
    onDecrement: () => setCount(prevState => prevState - 1)
  }
}

const Counter = (props) => {
  const counter = useCounter(props.count);
  return (
    <div>
      <p>{counter.count}</p>
      <button onClick={counter.onIncrement}>+</button>
      <button onClick={counter.onDecrement}>-</button>
    </div>
  );
}

const App = () => {
  return (
    <div>
      <Counter count={0} />
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Demo

動作は変わりませんが元のCounterからロジックが取り除かれました。この例ではメリットは感じられませんが、こうすることで再利用性が高まるわけです。

おわりに

現在、公式Hooksが基本を合わせて10個存在しています。クラスにある大体の機能は実装できそうです。また、サードパーティのHookも今後開発され、その中で開発に必須となるものも出てくると思います。

React HooksはReactの開発スタイルを根本から変えると言ってもいい機能なので、現状のノウハウやAPI周りを勉強し直していく必要があるのは間違いなさそうです。