React Hooks API の useEffect を使う

React のコンポーネントにはいくつかのタイミングで処理をオーバーライドするためのライフサイクルメソッドと呼ばれるインターフェースが提供されており、副作用を伴わせるようなカスタマイズが可能です。これらは JavaScript の class 構文で定義されたコンポーネントのメソッドをオーバーライドするなどの方法で利用できます。

以下はよくあるライフサイクルメソッドの利用例で、コンポーネントのマウント時 componentDidMount と props や state の更新時 componentDidUpdate にサーバーと通信して新しいデータを fetch しています。なお、この例ではネットワークリクエストをモックに置き換えています。

import React from "react";

type Props = {};
type State = { readonly category: string; readonly data: string[] };

const masterData: { [category: string]: string[] } = {
fruits: ["バナナ", "オレンジ", "りんご"],
mamal: ["猫", "犬", "馬"],
fish: ["サバ", "マグロ", "タラ", "サケ"]
};

class App extends React.Component<Props, State> {
state = { category: "fruits", data: [] };

// mock fetch
fetchData = (category: string) => setTimeout(() => this.setState({ data: masterData[category] }), 1000);

componentDidMount() {
this.fetchData(this.state.category);
}

componentDidUpdate(_0: Props, prevState: State) {
if (this.state.category !== prevState.category) {
this.fetchData(this.state.category);
}
}

onChange = (e: React.ChangeEvent<HTMLSelectElement>) =>> this.setState({ category: e.target.value });

render() {
const { category, data } = this.state;
return (
<div>
<select onChange={this.onChange} value={category}>
<option value="fruits">{"果物"}</option>
<option value="mamal">{"動物"}</option>
<option value="fish">{"魚"}</option>
</select>
<p>{"取得されたデータ"}</p>
<ul>
{data.map(value => (<li key={value}>{value}</li>))}
</ul>
</div>
);
}
}

React v16.8 から追加された Hooks API を使うと、これまではクラスコンポーネントで実現していたコンポーネントのライフサイクルに相当する処理を関数コンポーネントに対して記述することができます。

import React from "react";

type Props = {};

const masterData: { [category: string]: string[] } = {
fruits: ["バナナ", "オレンジ", "りんご"],
mamal: ["猫", "犬", "馬"],
fish: ["サバ", "マグロ", "タラ", "サケ"]
};

const App: React.FC<Props> = () => {
const [category, setCategory] = React.useState<string>("fruits");
const [data, setData] = React.useState<string[]>([]);

const onChange = (e: React.ChangeEvent<HTMLSelectElement>) =>
setCategory(e.target.value);

React.useEffect(
/* mock fetch */
() => { setTimeout(() => setData(masterData[category]), 1000); },
[category]
);

return (
<div>
<select onChange={onChange} value={category}>
<option value="fruits">{"果物"}</option>
<option value="mamal">{"動物"}</option>
<option value="fish">{"魚"}</option>
</select>
<p>{"取得されたデータ"}</p>
<ul>
{data.map(value => (<li key={value}>{value}</li>))}
</ul>
</div>
);
};

componentDidUpdate (及び componentDidMount や componentWillUnmount)に相当する処理を React.useEffect という API で代替することができます。

useEffect のインターフェース

useEffect は以下の形の引数を受け付けます。

React.useEffect(
() => {
/* エフェクト */
return () => { /* クリーンアップ処理 */ } /* optional */
},
[...deps] /* optional、エフェクトの依存関係 */
)

副作用(エフェクト)にクリーンアップ処理が必要な時(例えば、setTimeout 関数のタイマーを解除するなど)は、クリーンアップ処理を記述した関数を、関数である第1引数の戻り値に渡します。
また、props や state に対するエフェクトの依存関係を定義した値を配列として第2引数に渡すことで、エフェクトの発火のタイミングを様々に制御できます。

useEffect を使ったその他の実装例

useEffect に与える引数を変えてさらにいくつかの実装パターンを試してみます。

componentDidMount と componentWillUnmount 相当

第2引数に空の配列を与えると、エフェクトが props や state に依存しないことを宣言できます。コンポーネントのマウント時だけにエフェクトを実行し、アンマウント時だけにクリーンアップ処理を実行できます。以下は keydown イベントをサブスクライブする例です。

const App: React.FC = () => {
const [keys, setKeys] = React.useState("");
React.useEffect(() => {
const onKeyDown = (e: any) => setKeys(keys => keys + e.key);
window.addEventListener<any>("keydown", onKeyDown);
return () => { window.removeEventListener<any>("keydown", onKeyDown); }
}, []);
return (/* ビュー */);
};

コンポーネントがマウントされると、押下したキーが keys という state に記録されていきます。コンポーネントをアンマウントするとイベントリスナーは解除されます。

componentDidUpdate 相当

以下は、フォームに入力した値を非同期的にノーマライゼーションする例です。2秒後に入力したアルファベットが大文字になります。

const App: React.FC = () => {
const [text, setText] = React.useState("");

React.useEffect(
() => {
const timerId = setTimeout(() => {
setText(value => value.toUpperCase());
}, 2000);
return () => { clearTimeout(timerId); };
},
[text]
);

return (
<input type={"text"} value={text} onChange={(e: any) => setText(e.target.value)} />
);
};

新しく値が入力され text という state が更新されることで、クリーンアップ処理が実行され、古い setTimeout のタイマーが削除されます。 text を useEffect の第2引数に渡していることで、 text が更新された時だけにこの処理が発火することが保証されます。したがって、エフェクトが実行されるのは、最後にテキストが入力されてから2秒後のタイミングだけになります。

同等の処理をクラスコンポーネントで実現しようとすると、timerId を一旦 state に格納したり、 state の更新を判定する条件分岐などの処理が必要になりますが、それらを useEffect 関数だけで処理できていて、クラスコンポーネントに比べて記述がスマートになっているのが分かります。

WordPress との関連

WordPress 5.2 以降にバンドルされている Gutenberg は React v16.8 に依存しているため、Hooks API が使えるように思えますが、利用例などをまだ見聞きしておらず、どのように使えるのかよく分かりません。
https://github.com/WordPress/gutenberg/blob/master/packages/element/package.json#L28

@wordpress/compose などのヘルパーライブラリとの兼ね合いなどをよく研究して利用する必要がありそうです。