結論
一つのファイルでコンポーネントの定義とレンダーをしてはならないという話でした。
例えば、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です。
export const App = () => (
<h1>Hello World!</h1>
)
import { createRoot } from 'react-dom/client'
import { App } from './App.jsx'
const root = document.getElementById('react')
createRoot(root).render(<App />)
この編集をした上で、index.html
でJSXファイルを読み込んでいる箇所を修正すれば、無事にエラーが出なくなりました。
ちょっとファイル数が多くなるけど、まあしょうがないですね。
また、DOMContentLoaded
イベントを用いても解決することができます。
こちらの方法ならファイルを増やす必要はありません。
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 vite
(npx 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
タグを用意しておきます。
<!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
を作成します。
ちなみにこれは、最初の結論に書いたコードと同じものです。
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
を作成します。
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
を以下のように編集すれば出なくなります。
(詳しくは後述)
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コンポーネントを定義しているファイルか、
ということで、エクスポートを追加してみます。
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
とか。
ということで試してみました。
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
コンポーネントの場所を移した瞬間エラーが出るようになったということです。
まず、雛形(編集済み)のコードがこうなっています。
import React from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App.jsx'
const root = createRoot(document.getElementById('react'))
root.render(<App />)
export const App = () => (
<h1>Hello World!</h1>
)
そして、main.jsx
を編集し、App.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
を作成し、こちらにレンダリングの処理を移します。
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
からインポートされます。
export const App = () => (
<h1>Hello World!</h1>
)
最後に、index.html
のscript
タグを編集します。
<!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
イベントを用いて解決する方法もあります。
こちらの方法ならファイル分けをする必要がないので、少し楽かもしれません。
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
を以下のように編集します。
これは簡単なカウンターです。
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次第ということですね。知らんけど。