Open9

ファイル保存によって何が起きてる?hot updatedって何?

Yug (やぐ)Yug (やぐ)

このサイトで、serverUrlを変更しただけでレンダーされてる理由がわからない。
https://ja.react.dev/learn/lifecycle-of-reactive-effects#how-react-verifies-that-your-effect-can-re-synchronize

function createConnection(serverUrl: string, roomId: string) {
    return {
        connect() {
            console.log(
                '✅ Connecting to "' + roomId + '" room at ' + serverUrl + "..."
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' + roomId + '" room at ' + serverUrl
            );
        },
    };
}

const serverUrl = "https://localhost:12345";

function ChatRoom({ roomId }: { roomId: string }) {
    useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]);
    return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
    const [roomId, setRoomId] = useState("general");
    const [show, setShow] = useState(false);
    return (
        <>
            <label>
                Choose the chat room:{" "}
                <select
                    value={roomId}
                    onChange={(e) => setRoomId(e.target.value)}
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
            </label>
            <button onClick={() => setShow(!show)}>
                {show ? "Close chat" : "Open chat"}
            </button>
            {show && <hr />}
            {show && <ChatRoom roomId={roomId} />}
        </>
    );
}
Yug (やぐ)Yug (やぐ)

どうやら、serverUrl変更とか関係なくて、ただファイルを保存することで再レンダーが走っているらしい。何も変更せずともファイル保存したら再レンダーされてる。

つまり上のサイトは文字変えるたびに自動でファイル保存が走っている仕様だと思われる。

serverUrl変更->ファイル保存->再レンダー->useEffect実行

...ん?いやおかしいおかしい。useEffectの依存配列はroomIdなんだから、再レンダー起きたとしてもroomIdは変更されていないのでuseEffect内部は実行されないはず。

でもなぜファイル保存しただけで実行されるんだ?

Yug (やぐ)Yug (やぐ)

いや、というかファイル保存によって起こるviteのhot updatedって厳密には何をしてるんだ?という疑問が生まれたが、このhot updateが関係しているかも?

つまりhot updateというのは実は再レンダーではなくて、ファイル単位で再実行する的な?
レンダーより強力じゃない?これ

レンダーより強力でブラウザ再読み込みよりは弱いみたいなイメージかなぁ。

useEffectのコールバック関数が新しく作られるから依存配列が変わっていなくとも変わったと認識されて実行される、みたいな感じだと予想。

なので厳密には「変更」ではなく「誕生」だが、それもちゃんと検知するのがReactの特徴なのだろう、多分

Yug (やぐ)Yug (やぐ)

んでこのhot updatedってやつは、HMR(Hot Module Replacement)と同じっぽい。
ViteはデフォルトでHMRを提供している。

Vite はネイティブ ESM を介して HMR API を提供します。HMR 機能を備えたフレームワークは、API を活用して、ページを再読み込みしたり、アプリケーションの状態を損失することなく即座に正確な更新を提供できます。Vite は Vue の単一ファイルコンポーネント および React Fast Refresh に対しての HMR 統合を提供します。@prefresh/vite を介した Preact の統合された公式のライブラリーもあります。

https://ja.vite.dev/guide/features.html#hot-module-replacement

Yug (やぐ)Yug (やぐ)

多分コンポーネント単位ではなくファイル単位で再実行するという理解で良い気がする。

...でもuseEffectの依存配列が変わってないのに実行されるのはなぜだ?

「コールバック関数が毎回新しく作られるので依存配列が変わったかどうかは関係なしに実行される」とか変なこと思ってたけどよく考えたらそれはおかしいか。依存配列は常に関係あるはずだし。

useEffect処理の実行条件は

  1. マウント時
  2. 依存配列の値が変わったとき

のどちらかだけのはず。
だがファイル保存によりHMRが起きた時、そのどちらにも当てはまっていないので実行されるはずがないと思うのだが。

Yug (やぐ)Yug (やぐ)

HMRによってもはやマウントされなおしてるとか?つまりHMRするたびに毎回コンポーネントは0から誕生するのかも?

...だがそれを検証してみようとしてみたらすごい不思議な現象に出くわしたぞ

export default function App() {
  useEffect(() => {
    console.log('effect');
    return () => console.log('clean up');
  }, [])

  return (
    <h1>hello</h1>
  )
}

実行結果はこうなる

Yug (やぐ)Yug (やぐ)

最初のeffect->clean up->effectは理解できる。
(ここで出した仮説の通り)
https://zenn.dev/yg_kita/scraps/7279bd86ccc5c3

謎なのはhot updated(=ファイル保存=HMR)の後。

clean up->effectしか実行されていない。HMRするたびに毎回clean up->effectが実行される。
つまりマウントではなく更新されたとエフェクトは検知していることがわかる。
(マウントだと検知したならeffect->clean up->effectと出力されるはずなので)

しかし依存配列を[]にしているので更新という概念はこのエフェクトには無いはず。
依存配列を[]にしたエフェクトはマウント時のみ発火するはずだからだ。
https://ja.react.dev/reference/react/useEffect#passing-an-empty-dependency-array
https://ja.react.dev/learn/lifecycle-of-reactive-effects#what-an-effect-with-empty-dependencies-means

...圧倒的矛盾!なんだこれ!

Yug (やぐ)Yug (やぐ)

ちなみに親と子の関係はこんな感じになる

export default function App() {
  console.log('親のrender');
  useEffect(() => {
    console.log('親のeffect');
    return () => console.log('親のcleanup');
  }, [])

  return (
    <>
      <h1>parent</h1>
      <Child />
    </>
  )
}

function Child() {
  console.log('子のrender');
  useEffect(() => {
    console.log('子のeffect');
    return () => console.log('子のcleanup');
  }, [])

  return (
    <h1>child</h1>
  )
}

renderは親->親->子->子で、エフェクトは子->親->子->親という法則があるっぽい