@ 酒井悠宇
Reactで関数コンポーネントを書くときに何気なく使用する「useState」とか「useEffect」とかっていうあれは「Hooks」と呼ばれますが、あれが生まれるまでにいろんな歴史があったっぽいのでその辺をりあクトで理解していけたらと思います。
それではレッツラご!
Hooksは2019年に生まれたばかりの新しい技術で、既存のコードにはそれ以前の手法が使われているものが多い為、それらを読めなければ困るときが出てきそうです。だからHooksの歴史を学ぼうと思います。
Hooks誕生までを時系列順に並べると以下のような感じになります。
前提として、上記のこれらは「コンポーネントから state を伴ったロジックを切り出して再利用できるようにするためのロジック」です。
一つずつどんなものなのか調べながらまとめていきます!
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();
},
};
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(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 };
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}
/>
);
}
};
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>
);
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がやっていることを以下に簡単にまとめました。
HOCを使えばmixinsと違って状態とロジックを綺麗に切り分けられます。
また、今回の例のようにクラスコンポーネントを自前で用意せずとも関数コンポーネントにstateやライフサイクルメソッドを始めとする便利な機能を追加してくるHOCライブラリの「Recompose」というプロダクトも開発されました。
メジャーなライブラリのインターフェースとしても採用され、Recomposeが普及して一世を風靡したと言っていいHOCでしたが、公式は程なくHOCを推すのをやめて新しい方法を推奨するようになりました。
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,
});
}
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やRenderPropsはあくまで既存の技術を使用した設計パターンだったのに対して、Hooksはそうではなく、公式が新たにReactの機能として提供したものでした。
HOCやRender Propsがそれまで抱えていた問題としては、ロジックの追加が著しくコンポーネントツリーを汚染してしまうことです。
追加するロジックの文だけコンポーネントの階層が深くなってしまいます。
これはシンプルではないし、処理の流れを追いづらくしてしまいます。
対してHooksはコンポーネントにロジックを抱えた別のコンポーネントを被せるのではなく、コンポーネントのシステムの外に状態やロジックを持つ手段を提供したということです。
またHOCもRender Propsも状態を持つロジックを分離はできても、結局積極的に再利用できるほどには抽象化できていませんでした。
でもHooksを使えば、状態を持ったロジックを完全に任意のコンポーネントから切り離して、別のコンポーネントで再利用することが簡単にできるようになっています。
はい、というわけで今日はHooks誕生までの歴史を勉強してみました。
大体の流れがいい感じにわかったのでよかったです。
最後までごらんいただきありがとうございました!