かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Vite + React のプロジェクトでは require が使えない? Uncaught ReferenceError: require is not defined

Vite で作った React のアプリMSW を使おうとしてドキュメントにあるコード (下記)をコピペしたらエラーになった

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/browser');
  worker.start();
}
ReactDOM.render(<App />, document.getElementById('root'));

=> Uncaught (in promise) ReferenceError: require is not defined

require は node の方法 (CommonJS) なので ESM なクライアントサイドでは使えない

create-react-appcreate-next-app で作られたプロジェクトは webpack が使われていたので CommonJS の require もよしなに変換してくれていたので問題になってなかったが、Vite では DEV モードの時には ネイティブESM を使用するので require が使えないとなっていたっぽい

Vite は、ネイティブ ESM を行使してソースコードを提供します。ブラウザは、実質的にバンドラの仕事の一部を引き受けます
cf. なぜ Vite なのか | Vite

Dynamic Import を使う

require が使えないので ESModule の import を使えばOKということになります
今回は条件式の中で import したいので Dynamic import を使えば良さそうです

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
if (process.env.NODE_ENV === 'development') {
- const { worker } = require('./mocks/browser');
- worker.start();
+ (async () => {
+   const { worker } = await import('./mocks/browser');
+   worker.start();
+ })();
}
ReactDOM.render(<App />, document.getElementById('root'));

Dynamic import は非同期なので async の即時関数を作成してその中で import するようにすればOK
これで Vite のプロジェクトでも MSW の worker がどうさするようになりました。

おわり。


[参考]

そうだね…

今更の React 18 ReactDOM の render の使い方が変わった

React のアプリを出力する render の方法が React v18 からは変更になっています

React v18 からは createRoot を使ってアプリを出力する root になるオブジェクトを作成する必要がある

Before

import ReactDOM from 'react-dom';
ReactDOM.render(
  <App />,
  document.getElementById('app')
);

After (React v18)

import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('app'));
root.render(<App />);

import するのも react-dom から react-dom/client に変更になっているので注意

TypeScript を使う場合は document.getElementById('app')undefined になる可能性があるとして警告が出るので ! (Non-Null Assertion Operator) を使うと良い

import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('app')!);
root.render(<App />);

ReactDOM の変更に伴い SSR 時の API も色々変更になっているが、公式の Upgrade Guide 書かれているので深くは追わない

次からは <Suspense> とかの新機能をみていきたい
おわり。


[参考]

レンダ…

React の再レンダリングについてのメモ

React のコンポーネントレンダリング (Re-rendering) について試したのでメモ

前提: 親コンポーネントの state が更新されたら子コンポーネントは再レンダリングされる

import { FC, memo, useState } from 'react';

// 親と依存関係のないコンポーネント
const MyComponent: FC = () => {
  return <div>MyComponent</div>;
};

// 親から props が渡されるコンポーネント
type ButtonProps = {
  onClick: () => void;
  children: React.ReactNode;
};
const Button: FC<ButtonProps> = ({ onClick, children }) => {
  return <button type="button" onClick={onClick}>{children}</button>;
};

// メインのコンポーネント
export const Counter: FC = () => {
  const [count, setCount] = useState<number>(0);
  const handleIncrement = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <Button onClick={handleIncrement}>increment</Button>
      <MyComponent />
    </div>
  );
};

count が更新される度に Counter が再描画されその際に props が渡される <Button> も依存関係のない <MyComponent> も再描画される

props を受け取るコンポーネント (React.memouseCallback)

React.memo しても再レンダリングされるケース

propsObject.is で比較される。
onClick などのイベントハンドラはオブジェクトなのでレンダリング時に再生成される。これは React.memo じに渡された props とは異なるオブジェクトと判断されるので React.memo されたコンポーネントも再レンダリングされてしまう

import { FC, memo, useState } from 'react';

type ButtonProps = {
  onClick: () => void;
  children: React.ReactNode;
};
const Button: FC<ButtonProps> = ({ onClick, children }) => {
  return <button type="button" onClick={onClick}>{children}</button>;
};
// memo化
const MemoButton = memo(Button);

// メインのコンポーネント
export const Counter: FC = () => {
  const [count, setCount] = useState<number>(0);
  const handleIncrement = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <MemoButton onClick={handleIncrement}>increment</MemoButton>
      <MyComponent />
    </div>
  );
};

上記の例では <Button>React.memo でメモ化 (Memoization) していますが、<Counter> コンポーネントが再レンダリングされる度に、handleIncrement が再生成されるので <Counter> の再レンダリング時に <Button> も再レンダリングされてしまいメモ化の意味がありません

props のオブジェクトを useMemo, useCallback でメモ化する

オブジェクトを props として渡す場合、常に変化すつる訳ではないオブジェクトであれば useMemo, useCallback を使うことでメモされた同一のオブジェクトを渡すことができるようになる
先の例では const handleIncrement = () => voiduseCallback でメモ化することで <Button> コンポーネントに同じオブジェクトとして渡すことができる

- import { FC, memo, useState } from 'react';
+ import { FC, memo, useState, useCallback } from 'react';

// メインのコンポーネント
export const Counter: FC = () => {
  const [count, setCount] = useState<number>(0);
- const handleIncrement = () => {
+ const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
- };
+ }, []);
  // handleIncrement は他に依存がないので dependencies は `[]` (空配列)

  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <MemoButton onClick={handleIncrement}>increment</MemoButton>
      <MyComponent />
    </div>
  );
};

₍ ᐢ. ̫ .ᐢ ₎ yoshi!

props の場合 useCallback, useMemo でメモ化するだけではダメ

useCallback, useMemo でメモ化された prosp を渡しても、渡されるコンポーネント自体がメモ化されてないと親コンポーネントの再レンダリング時に再描画されてしまう

// メインのコンポーネント
export const Counter: FC = () => {
  const [count, setCount] = useState<number>(0);

  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  const handleDecrement = useCallback(() => {
    setCount((prevCount) => prevCount + -1);
  }, []);

  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <MemoButton onClick={handleIncrement}>increment</MemoButton>
      <Button onClick={handleDecrement}>decrement</Button>
      <MyComponent />
    </div>
  );
};

decrement ボタンのコンポーネントuseCallback でメモ化された handleDecrement を受け取るがコンポーネントがメモ化されてないので <Counter> の state が変化した際に親コンポーネントと一緒に再レンダリングされてしまう

親から props を渡されるコンポーネントのメモ化

props を渡されるコンポーネントReact.memo でメモ化した上で、prosp に含まれるオブジェクトが useCallback, useMemo でメモ化されている必要がある


依存関係のない子コンポーネント

React.memo でメモ化すれば再レンダリングされない

// 親と依存関係のないコンポーネント
const MyComponent: FC = () => {
  return <div>MyComponent</div>;
};
+ // メモ化
+ const MemoMyComponent = memo(MyComponent);

// メインのコンポーネント
export const Counter: FC = () => {
  const [count, setCount] = useState<number>(0);

  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <MemoButton onClick={handleIncrement}>increment</MemoButton>
-     <MyComponent />
+     <MemoMyComponent />
    </div>
  );
};

依存関係がないのでメモ化してしまえば当然、親コンポーネントの再描画されても再描画されなくなる
ただしメモ化にはコンポーネントに変化がないか計算する処理が含まれるので、必ずしもパフォーマンスが向上するわけではない

children として描画される依存関係のないコンポーネントは再レンダリングされない

ラッパーコンポーネントが増えてしまうが state を持つコンポーネント内で children として依存関係のないコンポーネントを描画するようにすれば、state が更新されても childrenReact.memo を使わなくても再描画されなくなる

// 親と依存関係のないコンポーネント
const MyComponent: FC = () => {
  return <div>MyComponent</div>;
};

// メインのコンポーネント
type CounterProps = {
  children: React.ReactNode;
};
const Counter: FC<CounterProps> = ({ children }) => {
  const [count, setCount] = useState<number>(0);

  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <MemoButton onClick={handleIncrement}>increment</MemoButton>
      {children}
    </div>
  );
};

// ラッパーコンポーネント
export const CounterContainer: FC = () => {
  return (
    <Counter>
      <MyComponent />
    </Counter>
  );
}

Counter コンポーネントと依存関係のない <MyComponent> はメモ化していないが、<Counter> が再描画される際にも再描画されることがない。おそらくラッパーである CounterContainer で呼び出されているのでこちらに紐付いて (CounterContainer の子という扱い?) いて、CounterContainer が再描画される際にしか再描画されないのではと思われる

所感

とりあえず useCallback, useMemo しておくか〜という感覚だったけど、props として渡す場合は子コンポーネントがメモ化されてないと抑制されるのは関数の再生成だけだったと改めて認識できた
Twitter で見かけた children を使う方法は知らなかったので、依存関係のないコンポーネントを内部に含めたいコンポーネントを作る際に活用したいと思いました

今回も長くなっちゃった。
キャプチャをたくさん撮ったけど Mac デフォルトだと mov なのでいい感じに gif でキャプれるアプリ知りたい…
おわり ₍ ᐢ. ̫ .ᐢ ₎


[参考]