0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

結論

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

例えば、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 

エラー概要

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

この記事は最近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 vitenpx create vite)は使わない
  • Reactのプラグイン(@vitejs/plugin-react)を入れたい

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

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

具体的な発生手順

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

プロジェクトを作成する

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

ターミナル
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 />)

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

ターミナル
bunx vite

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

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

プラグインを導入する

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

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

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

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

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

export default {
  plugins: [react()]
}

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

ターミナル
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.

原因

とりあえず、エラー文に書いてあったリンク先を読んでみます。
すると、こんなことが書いてありました。

For React refresh to work correctly, your file should only export React components. You can find a good explanation in the Gatsby docs.

React refreshが正しく動作するためには、ファイルはReactコンポーネントのみをエクスポートする必要があります。
DeepLで翻訳)

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

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

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

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を呼び出してしまう...?
つまりレンダリングをするファイルはモジュールだとダメなんでしょうか。

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

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

他に試したこと

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

ファイル名を変更する

Reactのコンポーネント名ってパスカルケースじゃないですか。
パスカルケースっていうのは例えばAppとかPascalCaseみたいなやつです。

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

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

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

先ほど書いた通り、Reactのコンポーネント名はパスカルケースです。
ちなみにこれはGatsbyのドキュメントにも明記されています。

それならパスカルケースじゃないコンポーネントならどうでしょうか。
例えば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.jsxAppコンポーネントの場所を移した瞬間エラーが出るようになったということです。

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

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からのインポートを削除しました。

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.htmlscriptタグを編集します。

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のこちらの回答を参考にしました。

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も実行されないということです。
ちなみにレンダリングは実施されたのでご安心ください。

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

プラグインを使わない

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

その場合は、当然ですがプラグインの恩恵が受けられなくなります。
その恩恵というのは、NPMのREADMEによるとこんな感じです。

  • 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 コンポーネントの状態を失うことなく編集できる機能です。
こちらを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次第ということですね。知らんけど。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?