概要
ちゃんと<Component .../>
形式で書かないとダメなようです。
経緯
先日少し作業が一段落したので、今更ながらreact-hooksを触っていました。
今まで作っていたもののうち1つを関数コンポーネント(以下FC)に置き換えようとし、さあ動確だとなったところ、
Uncaught Invariant Violation: Hooks can only be called inside the body of a function component.
というエラーが出て、上手く描画されない事態に遭遇しました。
誤解
確かに今までクラスコンポーネントで書いていた中、一部分だけを置き換えました。
また、利用側をFCに置き換えるとエラーが出ないので、「react-hooksを使うには全てFCにしないとダメなのか・・・?」と非常に焦りました。
コード例
生JSのクラスと異なりTSのクラスは色々と便利なので、FCに置き換えた際でもクラスを使うことはやめていませんでした。
(この例だと省いていますが、実際にはサポート用ロジックを色々書いていました。)
import * as React from 'react';
import * as ReactDOM from 'react-dom';
interface ChildComponentProps {
title: string;
}
class ChildComponent { // FC形式
public render(props: ChildComponentProps) { // このメソッドを呼び出すことで描画する
const [title] = React.useState(props.title);
return (
<div>{title}</div>
);
}
}
class ParentComponent extends React.Component { // CC形式
private _child = new ChildComponent();
public render() {
return (
<div>
{this._child.render({title: 'test'}}}
</div>
);
}
}
ReactDOM.render(<ParentComponent />, document.getElementById('app'));
エラー原因を調査するまで、単に関数やメソッド呼び出し等で仮想DOMを返せばFCになると思っていました。
解決方法
いろいろと調べた所、下の方と同じ現象に遭遇していることに気づきました。
このとき初めて、仮想DOMを関数で返すこと≠FCという根本的な誤解に気づきました。
<Component .../>
等の、Reactで取り扱う形式で記述して初めてFCとなるようです。
(確かにどこが境界なのかずっと疑問だったのと、Reactの仕組みを考えると当たり前なのですが…)
幾つか解決方法があるようですが、子のクラスインスタンスを保持する形式は変えたくなかったので、下記のようにReactNode
を返すgetterを定義し、呼び出すようにしました。
class ChildComponent {
private _node!: (props: IChildComponentProps)=>React.ReactElement; // useMemo等に影響があるので、同一の関数にする
public get createNode() { // このメソッドを追加
if(!this._node) {
this._node = (props: IChildComponentProps) => this.render(props)
}
return this._node;
}
public render(props: IChildComponentProps) {
const [title] = React.useState(props.title);
return (
<div>{title}</div>
);
}
}
class ParentComponent extends React.Component {
private _child = new ChildComponent();
public render() { // JSX形式にする
return (
<div>
<this._child.createNode title='test' />
</div>
);
}
}
追記
上に書いたクラスを正式化したものを作りました。
FCとCCの間の子みたいな感じになっています。