2021/08/26

Hooks誕生までのひすとりーが知りたい。

@ 酒井悠宇

Reactで関数コンポーネントを書くときに何気なく使用する「useState」とか「useEffect」とかっていうあれは「Hooks」と呼ばれますが、あれが生まれるまでにいろんな歴史があったっぽいのでその辺をりあクトで理解していけたらと思います。

それではレッツラご!

なぜHooksの歴史を学ぶのか

Hooksは2019年に生まれたばかりの新しい技術で、既存のコードにはそれ以前の手法が使われているものが多い為、それらを読めなければ困るときが出てきそうです。だからHooksの歴史を学ぼうと思います。

大体の流れ

Hooks誕生までを時系列順に並べると以下のような感じになります。

  1. Mixins
  2. HOC
  3. Render Props
  4. Hooks登場


前提として、上記のこれらは「コンポーネントから state を伴ったロジックを切り出して再利用できるようにするためのロジック」です。

一つずつどんなものなのか調べながらまとめていきます!

Mixins

Mixinsとは?

2013年に公開された当初のReactではコンポーネントはクラスしかなく、しかもそれを通常のクラス定義ではなく、React.createClassという静的メソッドを使って生成するようになっていました。
その引数はオブジェクトになっており、renderメソッドなどもそのプロパティとして実装していきます。
そしてmixinsというプロパティで、そのコンポーネントに追加したいクラスメンバーを任意のオブジェクトに格納して登録できました。
これが「mixins(ミックススイン)」という手法です。

具体的な使い方

以下の例はReact.createClassを用いた古いやり方です。mixinsを説明するために使用していますが、現在では非推奨とされています。

  • クラスのメンバーメソッドをオブジェクトとして切り出す
const CounterMixin = {
  //stateの初期化
  getInitialState: () => ({ count: 0 });
  //stateをリセットする関数
  reset: () => {
    this.setState({ count: 0 });
  },
  //stateに+1する関数
  increment: () => {
    this.setState((state) => ({ count: state.count + 1 }));
  },
  //現在のstateの最大値を監視するライフサイクル
  componentDidUpdate: () => {
    if (this.state.count > this.props.max) this.reset();
  },
};


  • それをクラスコンポーネントにmixinsプロパティにオブジェクトの配列形式で登録する
const CounterComponent = React.createClass({
  propTypes: {
    max: React.PropTypes.number.isRequired,
  },
  //ここに追加
  mixins: [CounterMixin],

  render: () => {
    const { count } = this.state;
    return (
      <div>
        <div>
          {count} / {max}
        </div>
        <button onClick={reset} type="button">
          Reset
        </button>
        <button onClick={increment} type="button">
          +1
        </button>
      </div>
    );
  }
});

export default CounterComponent;


jsxから呼び出すときはこんな感じで書きます。

<CounterComponent max={100} />


問題点

mixinsの問題点として挙げられるのは、コンポーネントとの間に暗黙的な依存関係を持ってしまうことです。

何故なら、ミックス先のコンポーネントに特定の名前のprops、特定の名前のstate、特定の名前のメンバー変数やメソッドがあることを前提とした記述になりがちだからです。
これではロジックの再利用には使いづらいです。

さらにmixinsは名前の衝突が起きやすいです。

何故なら、複数のmixinオブジェクトをフラットに配列として渡す、という使い方だからです。
これだとmixins間で名前の衝突が起きないか常に気を配る必要があります。

まとめ

一見するとシンプルで簡単なように見えるmixinsですが、コンポーネントと違って階層構造がなく、フラットな関係なため、プロジェクトの規模が大きくなればなるほどデータの流れや依存関係がどうなっているのかを把握するのも難しくなります。

これを受けてReact公式は2016年7月に、「Mixins Considered Harmful(有害とされるmixins)」という記事を出してmixinsの使用を控えるようにアナウンスしました。

そして翌2017年6月のバージョン16.6.0においてReact.createClassが非推奨になるのと同時にmixinsも廃止されました。

HOC

HOCとは?

HOC(Higher Order Cmponent)はReact公式ブログのmixinsにダメ出しした記事のなかで、その代わりに使うよう進められたもので、「高階コンポーネント」とも呼ばれます。
HOCはJavaScriptの高階関数の手法をコンポーネントに応用したもので、コンポーネントを引数に取り、戻り値としてコンポーネントを返す関数のことです。

おまけ的にですが、ロジックを注入するコンポーネントをcontainer component、ロジックは持たずに見た目部分を担当するコンポーネントをpresentational componentと呼びます。

具体的な使い方

HOCがどんな風にコンポーネントに機能を追加するのか、簡単なサンプルで見てみます。

type Props = { target: string };
const HelloComponent: FC<Props> = ({ target }) => <h1>Hello {target}!</h1>;
export default withTarget(HelloComponent);

const withTarget = (WrappedComponent: FC<Props>) => WrappedComponent({ target: 'Patty' });


以上の例では、HelloComponentがpropsとして受け取るtargetの実態をwithTargetというHOCで与えています。

HOCは特定のpropsを用いてコンポーネントに機能を追加します。なので、機能を注入される側のコンポーネントは、その名前のpropsを受け皿として用意しておく必要があります。

それでは先程mixinsによって実装したカウンターコンポーネントを、次はHOCによって実装してみます。

  • まずは型を定義します。
type InjectedProps = {
  count: number;
  reset: () => void;
  increment: () => void;
};
type Props = { max: number };
type State = { count: number };


  • ロジックを注入するHOC(container component)を定義します。
const withCounter = (WrappedComponent: FC<Props & Partial<InjectedProps>>) => {
  class EnhancedComponent extends Component<Props, State> {
    constructor(props: Props) {
      super(props);
      this.state = { count: 0 };
    }
    reset = (): void => this.setState({ count: 0 });
    increment = (): void => this.setState((state) => ({ count: state.count + 1 }));
    componentDidUpdate = (): void => {
      if (this.state.count > this.props.max) this.reset();
    };
    render = (): ReactElement => (
      <WrappedComponent
        max={this.props.max}
        count={this.state.count}
        reset={this.reset}
        increment={this.increment}
      />
    );
  }
};


  • HOCによってロジックを注入されるコンポーネント(presentational component)を定義します。
const CounterComponent: FC<Props & Partial<InjectedProps>> = ({
  max,
  count = 0,
  reset = () => undefined,
  increment = () => undefined,
}) => (
  <div>
    <div>
      {count} / {max}
    </div>
    <button onClick={reset} type="button">
      Reset
    </button>
    <button onClick={increment} type="button">
      +1
    </button>
  </div>
);


  • HOCによってロジックを注入されたコンポーネントをexportします
export default withCounter(CounterComponent);


まずCounterComponentから見ていきましょう。これは状態やロジックを持たない、純粋なpresentational componentです。

HOCであるwithCounterはこの中のcount、reset、incrementという3つのpropsに、外から状態やロジックを注入しているだけです。

このCounterComponentにprops型として渡されている、「Props & Partial<InjectedProps>」の説明をすると、
本来CounterComponentをJSXとしてマウントするとき必要なpropsはmaxだけですが、HOCによってcount、reset、incrementにロジックを注入できるようにしておく必要がある為、その3つを組み込みユーティリティー型のPartialで省略可能な形にして合成しています。

加えて内側のコンポーネントがそれ単体でも成立するように、HOCから注入される予定のpropsにはデフォルト値を設定しなければなりません。

次にCounterComponentにロジックを注入するwithCounterがやっていることを以下に簡単にまとめました。

  • withCounterはCounterComponentを引数として受け取っている
  • withCounterは内部で、CounterComponentで使用するstateやロジックをメンバーメソッドとして持つクラスコンポーネントを生成している
  • withCounterはクラスコンポーネント内のrenderメソッドで、引数として受け取ったコンポーネントにpropsとしてロジックを渡している。
  • withCounterは引数として受け取ったコンポーネントにロジックを注入したクラスコンポーネント自体を返り値として返す。
  • 結果、見た目と状態がHOCによって分離されたカウンターコンポーネントが完成する。


まとめ

HOCを使えばmixinsと違って状態とロジックを綺麗に切り分けられます。

また、今回の例のようにクラスコンポーネントを自前で用意せずとも関数コンポーネントにstateやライフサイクルメソッドを始めとする便利な機能を追加してくるHOCライブラリの「Recompose」というプロダクトも開発されました。

メジャーなライブラリのインターフェースとしても採用され、Recomposeが普及して一世を風靡したと言っていいHOCでしたが、公式は程なくHOCを推すのをやめて新しい方法を推奨するようになりました。

Render Props

Render Propsとは?

React公式がHOCを推すのをやめて新しい手法を推奨するようになりました。それが「Render Props」と呼ばれるものです。

これはReactElementsを返す関数をpropsとして受け取って、それを自身のレンダリングに使う特殊なコンポーネントを使う手法です。
レンダリングのための関数をpropsとして受け取るからrender propsと呼びます。

HOCは任意のコンポーネントを引数として受け取って、その戻り値にコンポーネントを返す関数ですが、render propsはReact Elementsを返す関数をpropsとして受け取ってそれを自身のレンダリングに利用するコンポーネントです。

正確にはrender propsとはその特殊なコンポーネントではなく、受け渡されてレンダリングに使われるpropsの方を指す言葉です。

具体的な使い方

Reader Propsがコンポーネントにどのようにロジックを注入するのかを理解するために簡単な例を以下に記述します。

type Props = { target: string };
const HelloComponent: FC<Props> = ({ target }) => <h1>Hello {target}!</h1>;
<TargetProvider render={HelloComponent} />
const TargetProvider: FC<{ render: FC<Props> }> = ({ render }) => render({ target: "Patty" });


HOCはコンポーネントを引数に受け取り、そのコンポーネントにロジックを注入するクラスごと返り値として返す形式でしたが、Render Propsは、引数に受け取ったコンポーネントを自身のレンダリングに使用するという形式のようです。

HOCで実装したカウンターコンポーネントをRender Propsでも実装してみましょう

  • 型を定義する
type ChildProps = {
  count: number;
  reset: () => void;
  increment: () => void;
};
type Props = {
  max: number;
  children: (props: ChildProps) => ReactElement;
};
type State = { count: number };


  • 状態やロジックを注入するクラスコンポーネントを定義する
class CounterProvider extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { count: 0 };
  }
  reset = (): void => this.setState({ count: 0 });
  increment = (): void => this.setState((state) => ({ count: state.count + 1 }));
  componentDidUpdate = (): void => {
    if (this.state.count > this.props.max) this.reset();
  };
  render = (): ReactElement =>
    this.props.children({
      count: this.state.count,
      reset: this.reset,
      increment: this.increment,
    });
}


  • 見た目部分を担当するpresentational componentを定義
const Counter: FC<{ max: number }> = ({ max }) => (
  <CounterProvider max={max}>
    {({ count, reset, increment }) => (
      <div>
        <div>
          {count} / {max}
        </div>
        <button onClick={reset} type="button">
          Reset
        </button>
        <button onClick={increment} type="button">
          +1
        </button>
      </div>
    )}
  </CounterProvider>
);


Render Props も根本の発想は HOC と同じで count、reset、increment という props に外側のコン ポーネントから状態やロジックを注入したいわけです。

ではなぜHOCがあるのに公式は新たにRender Propsのほうを押すようになったのでしょうか。

理由は次のようなところにあるみたいです。

  • HOC のように props の名前の衝突が起こりづらく、起こったとしてもコードから一目瞭然
  • TypeScript 使用時、props の型合成が必要ない
  • どの変数が機能を注入するための props として親コンポーネントに託されたのかがコードから 判別しやすい
  • コンポーネントの外で結合させる必要がなく、JSX の中で動的に扱うことができる


こんな感じみたいです。

Hooks誕生


HOCやRenderPropsはあくまで既存の技術を使用した設計パターンだったのに対して、Hooksはそうではなく、公式が新たにReactの機能として提供したものでした。

HOCやRender Propsがそれまで抱えていた問題としては、ロジックの追加が著しくコンポーネントツリーを汚染してしまうことです。
追加するロジックの文だけコンポーネントの階層が深くなってしまいます。
これはシンプルではないし、処理の流れを追いづらくしてしまいます。

対してHooksはコンポーネントにロジックを抱えた別のコンポーネントを被せるのではなく、コンポーネントのシステムの外に状態やロジックを持つ手段を提供したということです。

またHOCもRender Propsも状態を持つロジックを分離はできても、結局積極的に再利用できるほどには抽象化できていませんでした。

でもHooksを使えば、状態を持ったロジックを完全に任意のコンポーネントから切り離して、別のコンポーネントで再利用することが簡単にできるようになっています。

最後に

はい、というわけで今日はHooks誕生までの歴史を勉強してみました。
大体の流れがいい感じにわかったのでよかったです。
最後までごらんいただきありがとうございました!

© 2021 powerd by UnReact