💿

Remixでreact-konvaを使う

2024/05/16に公開

Full Stack Web FrameworkのRemixに、Canvasを扱うライブラリreact-konvaを導入する方法について記載します。

https://remix.run/

https://konvajs.org/

Remixのプロジェクトは作成済みで、パッケージ管理のためにpnpmを利用しているものとします。

react-konvaをインストールします。

pnpm install react-konva konva

https://github.com/konvajs/react-konva

READMEに従い、Canvasを描画するコンポーネントを作成します。

app/routes/sample/components/my-canvas.tsx
import { Circle, Layer, Rect, Stage } from "react-konva";

export const MyCanvas = () => {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Rect width={50} height={50} fill="red" />
        <Circle x={200} y={200} stroke="black" radius={50} />
      </Layer>
    </Stage>
  );
};

呼び出し元のページに先ほどのコンポーネントを追加します。

app/routes/sample/index.tsx
import { json } from "@remix-run/node";
import { MyCanvas } from "./components/my-canvas";

export const loader = () => {
  return json({});
};

export default function Index() {
  return (
    <div>
      <MyCanvas />
    </div>
  );
}

pnpm dev でdev serverを起動し対象ページにアクセスとすると、次のようなエラーとなります。

pnpm preview で一旦buildしたものをプレビューすると、次のようなエラーとなります。

Canvas周りはSSRと相性が悪いようで、CanvasのみClient Sideで処理するよう指定する必要があります。

Canvasはブラウザで動作するためDOMのwindowが必要です。条件付きレンダリングしてあげます。

app/routes/sample/index.tsx
import { json } from "@remix-run/node";
import { MyCanvas } from "./components/my-canvas";

export const loader = () => {
	return json({});
};

export default function Index() {
  return (
    <div>
-     <MyCanvas />
+     {typeof window !== "undefined" && <MyCanvas />}
    </div>
  );
}

更に、Canvasを描画するコンポーネントの名前を my-canvas.tsx から my-canvas.client.tsx に変更します。

これによりコンポーネントはClient modulesとして扱われるため、Canvasが描画できるようになります。

https://remix.run/docs/en/main/file-conventions/-client

以下、参考となったIssueです。

https://github.com/remix-run/remix/discussions/1023

しかし、これでもまだエラーが残ります。

どうやら {typeof window !== "undefined" && <MyCanvas />} での条件付きレンダリングがうまくいっていないようです。

https://react.dev/reference/react-dom/hydrate#suppressing-unavoidable-hydration-mismatch-errors

Reactのhydrationの公式Docに従い、 window ではなく Client Sideでのみ実行される useEffect を利用してCanvasを描画するようにします。

app/routes/sample/index.tsx
+import { useEffect, useState } from "react";
import { json } from "@remix-run/node";
import { MyCanvas } from "./components/my-canvas";

export const loader = () => {
  return json({});
};

export default function Index() {
+ const [isClient, setIsClient] = useState(false);
+ useEffect(() => {
+   setIsClient(true);
+ }, []);

  return (
    <div>
-     {typeof window !== "undefined" && <MyCanvas />}
+     {isClient ? (
+       <MyCanvas />
+     ) : <div />}
    </div>
  );
}

これで全てのエラーが解消され、Canvasが安定して描画されるようになりました。

Discussion