TypeScript
React
react-hooks

react hooksを触って、"Hooks can only be called inside the body of a function component"が出た時の話


概要

ちゃんと<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になると思っていました。


解決方法

いろいろと調べた所、下の方と同じ現象に遭遇していることに気づきました。

https://medium.com/@jonchurch/how-to-fix-react-error-rendered-fewer-hooks-than-expected-e6a378985d3c

このとき初めて、仮想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>
);
}
}