【Vite】Reactのプラグインを入れたらCould not Fast Refresh.と言われた

# 結論

**一つのファイルでコンポーネントの定義とレンダーをしてはならない**という話でした。

例えば、`App.jsx`が以下のようになっているとします。

```App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => (
  <h1>Hello World!</h1>
)

const root = document.getElementById('react')
createRoot(root).render(<App />)
```

このコードはプラグインが入っていない環境だと問題なく動きます。
しかし、プラグインを入れるとエラーが出るようになりました。

解決楽は簡単です。
新しく`main.jsx`を作り、レンダリングのコードはそっちに移せばOKです。

```App.jsx
export const App = () => (
  <h1>Hello World!</h1>
)
```

```main.jsx
import { createRoot } from 'react-dom/client'
import { App } from './App.jsx'

const root = document.getElementById('react')
createRoot(root).render(<App />)
```

この編集をした上で、`index.html`でJSXファイルを読み込んでいる箇所を修正すれば、無事にエラーが出なくなりました。

ちょっとファイル数が多くなるけど、まあしょうがないですね。

* * *

また、`DOMContentLoaded`イベントを用いても解決することができます。
こちらの方法ならファイルを増やす必要はありません。

```App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => (
  <h1>Hello World!</h1>
)

document.addEventListener('DOMContentLoaded', () => {
  const root = createRoot(document.getElementById('react'))
  root.render(<App />)
})

export default App 
```


# エラー概要

遅くなりましたがどんなエラーが発生したのか書きます。

:::note warn
この記事は最近Reactを勉強し始めた初心者が書いています。
そのため内容に間違いがある可能性があります。
参考にする際は**一度自分でしっかり調べることをお勧めします。**

また、間違いがありましたらコメントなどでご指摘いただけたら幸いです。
:::


## 環境

* MacOS
* エディタ：VSCode
* ブラウザ：Vivaldi
* Vite 5.3.1 
* React 18.3.1
* Reactプラグイン（`@vitejs/plugin-react`）：4.3.1
* ランタイムにはBun 1.1.17 を使用


## やりたかったこと

* `bun create vite`（`npx create vite`）は使わない
* Reactのプラグイン（`@vitejs/plugin-react`）を入れたい

:::note info
なぜ`bun create vite`を使わないのかというと、作るもの的にファイルを1から作った方が楽だったからです。

プロジェクトの雛形を作ってくれる機能は、確かに便利です。
ですが、ただ`Hello World!`を表示したい場面では、雛形がない方が私はむしろ楽でした。
:::


## 具体的な発生手順

事前にプロジェクトフォルダを作成し、そのフォルダをVSCodeなどのエディタで開いておきます。


### プロジェクトを作成する

まずはReactとViteをインストールします。

```sh:ターミナル
bun add react react-dom vite
```

次に`index.html`を作成します。
この際、Reactのコンテナとなる`main#react`タグと、JSXファイルを読み込む`script`タグを用意しておきます。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>てすと</title>
</head>
<body>
  <main id="react"></main>
  <script type="module" src="./App.jsx"></script>
</body>
</html>
```

そして`App.jsx`を作成します。
ちなみにこれは、最初の結論に書いたコードと同じものです。

```App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => (
  <h1>Hello World!</h1>
)

const root = document.getElementById('react')
createRoot(root).render(<App />)
```

ここで一度開発サーバーを立ち上げてみます。

```sh:ターミナル
bunx vite
```

この状態で http://localhost:5173 を開くと、`Hello World!`が表示されているはずです。

また、`App.jsx`などを編集してホットリロードさせても**エラーが出力されない**ことを確認します。


### プラグインを導入する

では本題のReactプラグイン（`@vitejs/plugin-react`）を導入します。

まずはインストールします。

```sh:ターミナル
bun add @vitejs/plugin-react
```

次に`vite.config.js`を作成します。

```vite.config.js
import react from '@vitejs/plugin-react'

export default {
  plugins: [react()]
}
```

そして開発サーバーを立ち上げます。

```sh:ターミナル
bunx vite
```

サーバーは無事に立ち上がると思います。
が、ホットリロードが発生すると、以下のエラーがターミナルに出力されます。

```:ターミナル
[vite] hmr invalidate /App.jsx Could not Fast Refresh. 
Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports
```


## 修正したら別のエラーが

上記のエラーですが、一応`App.jsx`を以下のように編集すれば出なくなります。
（詳しくは後述）

```App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => (
  <h1>Hello World!</h1>
)

const root = document.getElementById('react')
createRoot(root).render(<App />)

// エクスポートを追加する
export default App 
```

この編集をすると、ターミナルのエラーは消えます。
が、その代わり**ブラウザのコンソールに**以下のエラーが出力されます。

```
Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. 
Instead, call root.render() on the existing root instead if you want to update it.
```


# 原因

とりあえず、[エラー文に書いてあったリンク先](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports)を読んでみます。
すると、こんなことが書いてありました。

> For React refresh to work correctly, your file should only export React components. You can find a good explanation in the [Gatsby docs](https://www.gatsbyjs.com/docs/reference/local-development/fast-refresh/#how-it-works).

> React refreshが正しく動作するためには、ファイルはReactコンポーネントのみをエクスポートする必要があります。
> （[DeepL](https://www.deepl.com/ja/translator#en/ja/)で翻訳）

どうやら、ファイルはReactコンポーネントをエクスポートする必要があるそうです。
「Reactコンポーネントのみ」と書かれていますが、**何もエクスポートしていないのも問題**なのかもしれません。

といはいえ、この「ファイル」に当てはまるファイルがよくわかりません。
よくわからないのですが、考えてても答えは出てこなそうでした。
なのでReactコンポーネントを定義しているファイルか、

ということで、エクスポートを追加してみます。

```diff_jsx:App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => (
  <h1>Hello World!</h1>
)

const root = document.getElementById('react')
createRoot(root).render(<App />)

+ // エクスポートを追加する
+ export default App 
```

この状態でサーバーを起動しなおし、ホットリロードを発生させてみます。
すると、ターミナルにエラーが出なくなりました。

これで解決かと思いきや、今度は別のエラーがブラウザのコンソールに出力されました。

```:ブラウザのコンソール
Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. 
Instead, call root.render() on the existing root instead if you want to update it.
```

これも初回は発生せず、ホットリロード時にだけ出力されてしまいます。

どうやら、`createRoot`を複数回呼び出しているというエラーの模様です。
実際のコードでは複数回呼び出してなんてないのですが...どういうことでしょう。

調べてもよくわからなかったので推測になってしまいますが、おそらく以下の点が関係していると思われます。

* `export`を追加したことによって、`App.jsx`がモジュールだと認識された
* エラー内容的に、ホットリロード時に`createRoot`を呼び出している可能性が高い

ということはつまり、モジュールだとなぜかホットリロード時に`createRoot`を呼び出してしまう...?
つまりレンダリングをするファイルはモジュールだとダメなんでしょうか。

:::note warn
これは初心者が何の情報源も参考にせず考えついた推測ですので、間違っている / 正確ではない可能性があります。

参考にする際は一度自分でしっかりと調べることを推奨します~~が、そもそも調べて簡単に出てくるなら苦労しないんだよなぁ...~~
:::


## 他に試したこと

これ以外にもいくつか試したことがあるので、まとめておきます。


### ファイル名を変更する

Reactのコンポーネント名って[パスカルケース](https://wa3.i-3-i.info/word13954.html)じゃないですか。
パスカルケースっていうのは例えば`App`とか`PascalCase`みたいなやつです。

そして、`App.jsx`というファイル名もパスカルケースです。
ならもしかして、**ファイル名によって`App.jsx`がコンポーネントと認識されている**のではないか...?
と思い、ファイル名を`main.jsx`に変えてみました。

...結果は、ファイル名を変える前と**何も変わりません**でした。
どうやらファイル名は関係なさそうです。


### コンポーネント名を変える

先ほど書いた通り、Reactのコンポーネント名はパスカルケースです。
ちなみにこれは[Gatsbyのドキュメント](https://wa3.i-3-i.info/word13954.html)にも明記されています。

それならパスカルケースじゃないコンポーネントならどうでしょうか。
例えば`snake_case`とか。

ということで試してみました。

```App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const snake_case = () => (
  <h1>Hello World!</h1>
)

const root = createRoot(document.getElementById('react'))
root.render(<snake_case />)
```

すると、以下のワーニングがブラウザのコンソールに出ました。

```:コンソール
Warning: The tag <snake_case> is unrecognized in this browser. 
If you meant to render a React component, start its name with an uppercase letter.
```

...どうやらReactのコンポーネントはパスカルケース（`uppercase`とも言う）で書かなきゃダメみたいですね。
この方法はあきらめましょう。

ちなみにターミナルの方のエラーは出なくなりました。~~ちょっと悲しい。~~


### 徹底的に調べる

ここまで来たら原因が見当もつかなかったので、一度絶対に原因を見つけられる手法を試すことにしました。

その手法は、現在のプロジェクトを**雛形で上書き**して、GitのDiffを頼りに**元のプロジェクトに近づけていく**と言うものです。
生成された雛形でエラーが出ないのは確認済みでしたので、これなら近づけていくどこかの過程でエラーが出るようになるはずです。

やってみた結果わかったのは、`main.jsx`から`App.jsx`に`App`コンポーネントの場所を移した瞬間エラーが出るようになったということです。

まず、雛形（編集済み）のコードがこうなっています。

```main.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App.jsx'

const root = createRoot(document.getElementById('react'))
root.render(<App />)
```

```App.jsx
export const App = () => (
  <h1>Hello World!</h1>
)
```

そして、`main.jsx`を編集し、`App.jsx`からのインポートを削除しました。

```diff_jsx:main.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'
- import { App } from './App.jsx'

+ export const App = () => (
+   <h1>Hello World!</h1>
+ )

const root = createRoot(document.getElementById('react'))
root.render(<App />)
```

この変更をしたところ、前述のエラーが発生するようになりました。

ここから私は、**コンポーネントの定義とレンダリングを同じファイルにまとめる**とエラーになるのではないか？と推測します。


# 解決策

実は解決策は2つあります。


## ファイルを分ける

最も王道なのは、**コンポーネントの定義とレンダリングでファイルを分ける**ことでしょう。
Viteの雛形がそうなっているので、これで問題が起こることは少ないと思います。

まずは`main.jsx`を作成し、こちらにレンダリングの処理を移します。

```main.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import { App } from "./App";

const root = createRoot(document.getElementById('react'))
root.render(<App />)
```

次に`App.jsx`内で`App`コンポーネントを定義します。
これは`main.jsx`からインポートされます。

```App.jsx
export const App = () => (
  <h1>Hello World!</h1>
)
```

最後に、`index.html`の`script`タグを編集します。

```diff_html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>てすと</title>
</head>
<body>
  <main id="react"></main>
- <script type="module" src="./App.jsx"></script>
+ <script type="module" src="./main.jsx"></script>
</body>
</html>
```

この変更をすることで、ターミナルにもコンソールにもエラーが表示されなくなるはずです。


## イベントで解決する

Stack Overflowのこちらの回答を参考にしました。

https://stackoverflow.com/a/72139826

`DOMDontentLoaded`イベントを用いて解決する方法もあります。
こちらの方法ならファイル分けをする必要がないので、少し楽かもしれません。

```App.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => (
  <h1>Hello World!</h1>
)

document.addEventListener('DOMContentLoaded', () => {
  const root = createRoot(document.getElementById('react'))
  root.render(<App />)
})

export default App 
```

* レンダリングの処理は`document.addEventListener`内で行う
* `export default App`のようにエクスポートを書く

のがポイントです。

推測ですが...
まず、ホットリロード時には`DOMContentLoaded`イベントは発生しません。
そのため中の関数も実行されず、結果的に`createRoot`も実行されないということです。
ちなみにレンダリングは実施されたのでご安心ください。

:::note warn
この方法を使ったことで正常に動作する保証はありません。
公式で紹介されている方法ではないので、使うなら自己責任でお願いします。
:::


## プラグインを使わない

いっそのことプラグインを使わないのもいいかもしれません。

その場合は、当然ですがプラグインの恩恵が受けられなくなります。
その恩恵というのは、[NPMのREADME](https://www.npmjs.com/package/@vitejs/plugin-react)によるとこんな感じです。

> * enable Fast Refresh in development (requires react >= 16.9)
> * use the automatic JSX runtime
> * use custom Babel plugins/presets
> * small installation size

> DeepLによる翻訳：
> * 開発時にFast Refreshを有効にする（react >= 16.9が必要）
> * 自動JSXランタイムを使用する。
> * カスタムBabelプラグイン/プリセットを使用する。
> * インストールサイズが小さい


### Fast Refreshについて

> Fast Refresh は、実行中のアプリケーションで React コンポーネントの状態を失うことなく編集できる機能です。
> （[こちら](https://www.npmjs.com/package/react-refresh)をDeepLで翻訳して引用）

...とのことですが、文章で見てもよくわからなかったので、簡単なカウンターを作って試してみました。

* 解決策の「ファイルを分ける」の状態からスタート
* `bun remove @vitejs/plugin-react`でプラグインを削除し、`vite.config.js`を編集（削除）しておく

まずは`App.jsx`を以下のように編集します。
これは簡単なカウンターです。

```App.jsx
import React, { useState } from "react";

export const App = () => {
  const [count, setCount] = useState(0)
  return (<>
    <p>count: {count}</p>
    <button onClick={() => setCount(c => c + 1)}>add</button>
  </>)
}
```

次に、これをブラウザで表示し。addボタンを押すとカウントが増えるのを確認します。
この際カウントの値は初期値である`0`以外にしておいてください。

最後に、`App.jsx`を編集（保存されればOK）して、カウントの値がどうなるのかを見ます。
`0`になっていれば状態がリセットされていて、元のカウントならリセットされていません。

結果はというと、私の環境ではカウントが`0`になりました。
これは**状態がリセットされている**証拠ですね。


#### プラグインありの状態

一応プラグインを入れ直して確認します。
`bun add @vitejs/plugin-react`を実行し、`vite.config.js`を作り直し、開発サーバーを立ち上げました。

先ほどと同じようにカウントを増やしてホットリロードしたところ、カウントの値がそのままになっていました。
これは**状態が保存されている**証拠です。
Reactのプラグインってすごいですね。

* * *

便利とはいえ、なくても開発はできます。
特に小規模なアプリを作る場合はそこまで困らないでしょう。


### プラグインなしでもできること

では逆にプラグインなしでは何ができるのかというと、こんな感じです。

* 開発サーバーの立ち上げ
* ホットリロード（状態はリセットされる）
* ビルド

とりあえず一通りのことはできるので、案外なくても何とかなります。
ですが当然あった方が色々といいことがあるのは間違いないでしょう。

プラグインを入れるかどうかはTPO次第ということですね。知らんけど。