ReactのClass Componentでthisを参照しつつhooksを使うワークアラウンド
はじめに
ReactのFunction Componentにhooksが導入されてから、既に5年ほど経ちました。Class Componentを使う機会はほぼなくなってしまった、という方も多いと思います。公式ドキュメントでもClass Componentは非推奨な扱いになっていて、Function Componentへの移行が推奨されています。Next.jsの実装するRSCではClass ComponentをServer Componentとして使うことが許可されていなかったりもします。
実際、componentDidCatchやgetSnapshotBeforeUpdateなど一部Class Componentにしか用意されていないlifecycle methodはまだあるものの、普通に使っている分にはFunction Componentで事足りるため、昔作成したClass ComponentをFunction Componentへ書き直しを進めている/既に書き直しを完了した方も多いと思います。
移行作業について、近年ではChatGPTやGitHub Copilotにやらせることもできますし、react-declassifyのようなツールもあります。一昔前よりは移行もやりやすくなっていると思います。
ただ、シンプルなケースであれば何の問題もないですが、Class Component特有の挙動に依存した実装がある場合、単純な変換が難しいケースがあります。
例えばcallback内からpropsにアクセスしたいケースを想定すると、Class Componentではthis
を介して常に最新のpropsにアクセスできますが、Function Componentではクロージャ内にあるrender時点のpropsにしか通常はアクセスできません。
勿論、Reactは後方互換性を十分担保しながらアップデートを行っているので、敢えてClass Componentのまま移行しないという判断もありうると思います。現に、かつてdeprecatedになったcreateReactClass
で書かれたcomponentも、shimを使えば動きます。Class Componentも将来的にこのような扱いになっていく可能性もあると思います。
でも私はhookが使いたい…少しずつでも移行を進めたい…そんな時に使えるかもしれない、ですがあまりお勧めはできないワークアラウンドです。
実例
以下がよくあるClass Componentの例です。
interface HelloProps {
foo: string;
}
interface HelloState {
bar: boolean;
}
class Hello extends Component<HelloProps, HelloState> {
constructor(props: HelloProps) {
super(props);
this.state = { bar: false };
}
onClick = () => {
this.setState((prev) => ({ bar: !prev.bar }));
};
render() {
return (
<button onClick={this.onClick}>
{this.state.bar ? this.props.foo : "hello"}
</button>
);
}
}
以下が上記のComponentを出来る限りそのままに、useEffectを導入してみた例です。
interface HelloProps {
foo: string;
}
interface HelloState {
bar: boolean;
}
class Hello extends Component<HelloProps, HelloState> {
constructor(props: HelloProps) {
super(props);
this.state = { bar: false };
}
onClick = () => {
this.setState((prev) => ({ bar: !prev.bar }));
};
render() {
return <this.Inner />;
}
Inner = () => {
useEffect(() => {
console.log('Effect!');
}, [])
return (
<button onClick={this.onClick}>
{this.state.bar ? this.props.foo : "hello"}
</button>
);
};
}
ポイントは、propsやstateが更新された時に必ずrender()
が実行される点、(memo
などを使っていない限り)render()
配下のcomponentは全て再renderingされる点です。
なので、このようにClass Componentのinstance fieldとしてFunction Componentを持つことで、元の実装を出来る限り残したままhookが使えます。
デメリットとしては、eslintが正しく効かないケースがあります。例えばuseEffectの中でthis.props.foo
を参照しているとして、depsにthis.props.foo
を含め忘れても、eslint-plugin-react-hooks
に怒られません。
あとはReactが想定していないだろう方法なので、何か実装を誤った場合、想定通りレンダリングされない可能性はあるかもしれません。Suspenseなどconcurrent featureと絡めた時に、何か不都合があるかもしれません。一応この状態のcomponentを数ヶ月間productionで動作させていますが、今の所はこれに起因する問題は確認されていません。
おわりに
こんな方法を使わないで済むならその方が良いです。さっさとFunction Componentに移行しましょう。
Discussion