RemixでMaterial UIのJoy UIを使えるようにする!

2024/09/07 10:01

「Remix」「Material UI」「Joy UI」を使えるようにしたいなと思って環境を作っていたら、公式のサンプルが古かったり、警告やエラーがなかなか消えなくて苦労しました、、
時間はかかりましたが、解決できて問題なく使えるようにできたので記事にまとめます!

同じく「Joy UI」使いたいけどエラーで困ったって人がいたら参考にしてください😊

TL;DR

  • このリポジトリをクローンすれば、動かせるようにしました!
  • 「Viteでの依存関係」「サーバーとクライアントでクラス名が変わってしまう」のが原因
  • 「Viteの設定で依存関係について記載」「Emotionのキャッシュでクラス名が変わらないように」で解決!

その前に

今回の内容を実際に動かしてみるには、「Node.js」のインストールが必須になります。
もしまだインストールしていないという人は、こちらの記事を参考にインストールをしておいてください!

前提

「Remix」のプロジェクトがある前提で説明をしていきます。
もしまだプロジェクトを作っていないという人は、公式のクイックスタートを参考にプロジェクトを作っておいてください!
「Remix」のプロジェクトの作り方について、別記事で解説します!)

プロジェクトを整理する

「Joy UI」をインストールする前にプロジェクトを整理しておきます。

今回使わない「Tailwind」と「PostCSS」のアンインストール、「Sass」のインストールを行います。

コマンド

# 「Tailwind」と「PostCSS」のアンインストール
npm uninstall tailwindcss postcss
rm tailwind.config.ts postcss.config.js app/tailwind.css

# 「Sass」のインストール
npm install sass
// app/root.tsx

import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
- import "./tailwind.css";

export function Layout({ children }: { children: React.ReactNode }) {
...
// vite.config.ts

+ import path from 'path';
+ 
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
+   resolve: {
+     alias: {
+       '~': path.resolve(__dirname, 'app'),
+     },
+   },
  plugins: [
...

「Joy UI」をインストールする

プロジェクトの整理も終わったので、早速「Joy UI」をインストールしていきましょう!

npmパッケージをインストールする

まずは必要なnpmパッケージをインストールします。
(ここで問題になる「Emotion」が出てきます。)

npm install @mui/joy @emotion/react @emotion/styled

「Joy UI」に関するファイルを準備する

「Joy UI」の設定に必要なファイルを4つ準備します。

スタイルをリセットする normalize.css の代わりに、公式が用意している CssBaseline を読み込みます。

// app/joy-ui/document.tsx

import CssBaseline from "@mui/joy/CssBaseline";

import type React from "react";

export interface Props {
  children: React.ReactNode;
}

export const JoyUiDocument: React.FC<Props> = ({ children }) => {
  return (
    <>
      <CssBaseline />
      {children}
    </>
  );
};

Googleフォントなどを読み込むために、 links を用意しておきます。

// app/joy-ui/links.ts

import type { LinksFunction } from "@remix-run/node";

export const joyUiLinks: LinksFunction = () => [
  { rel: "preconnect", href: "https://fonts.googleapis.com" },
  {
    rel: "preconnect",
    href: "https://fonts.gstatic.com",
    crossOrigin: "anonymous",
  },
  {
    rel: "stylesheet",
    href: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap",
  },
];

「Emotion」が原因で「サーバーとクライアントでクラス名が変わってしまう」エラーが出てしまうので、キャッシュにスタイルを残すようにします。

// app/joy-ui/provider.tsx

import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { CssVarsProvider } from "@mui/joy/styles";

import theme from "./theme";

import type React from "react";

function createEmotionCache() {
  return createCache({ key: "css" });
}

export interface Props {
  children: React.ReactNode;
}

export const JoyUiProvider: React.FC<Props> = ({ children }) => {
  const cache = createEmotionCache();

  return (
    <CacheProvider value={cache}>
      <CssVarsProvider theme={theme}>{children}</CssVarsProvider>
    </CacheProvider>
  );
};

カラーなどをカスタマイズできるように、テーマに関するファイルを準備しておきます。

// app/joy-ui/theme.ts

import { extendTheme } from '@mui/joy/styles';

declare module '@mui/joy/styles' {
}

const theme = extendTheme({
  colorSchemes: {
    light: {
      palette: {},
    },
    dark: {
      palette: {},
    },
  },
});

export default theme;

「Joy UI」が使えるように変更する

「Joy UI」に関するファイルの準備ができたら、最後に元々あったファイルに読み込ませていきます。

クライアントでクラス名が変わらないように、キャッシュからスタイルを読み込むようにします。

// app/entry.client.tsx

...
import { hydrateRoot } from "react-dom/client";

+ import { JoyUiProvider } from "~/joy-ui/provider";
+ 
startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
-       <RemixBrowser />
+       <JoyUiProvider>
+         <RemixBrowser />
+       </JoyUiProvider>
    </StrictMode>
  );

サーバーでクラス名が変わらないように、キャッシュからスタイルを読み込むようにします。

// app/entry.server.tsx

...
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";

+ import { JoyUiProvider } from "~/joy-ui/provider";
+ 
const ABORT_DELAY = 5_000;

export default function handleRequest(

...

 return new Promise((resolve, reject) => {
    let shellRendered = false;
    const { pipe, abort } = renderToPipeableStream(
-       <RemixServer
-         context={remixContext}
-         url={request.url}
-         abortDelay={ABORT_DELAY}
-       />,
+       <JoyUiProvider>
+         <RemixServer
+           context={remixContext}
+           url={request.url}
+           abortDelay={ABORT_DELAY}
+         />
+       </JoyUiProvider>,
      {
        onShellReady() {
          shellRendered = true;
...

メタ情報や、スタイルのリセットを行います。

// app/root.tsx

...
  ScrollRestoration,
} from "@remix-run/react";

+ import { JoyUiDocument } from "~/joy-ui/document";
+ import { joyUiLinks } from "~/joy-ui/links";
+ import { JoyUiMeta } from "~/joy-ui/meta";
import style from "~/styles/common.scss?url";

import type { LinksFunction } from "@remix-run/node";

- export const links: LinksFunction = () => [{ rel: "stylesheet", href: style }];
+ export const links: LinksFunction = () => [
+   ...joyUiLinks(),
+   { rel: "stylesheet", href: style },
+ ];

export const Layout = ({ children }: { children: React.ReactNode }) => {
  return (

...

        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
+         <JoyUiMeta />
        <Links />
      </head>

...

const App = () => {
-   return <Outlet />;
+   return (
+     <JoyUiDocument>
+       <Outlet />
+     </JoyUiDocument>
+   );
};

export default App;

「Vite」「Joy UI」の依存関係が外部化されないようにします。

// vite.config.ts

...
    }),
    tsconfigPaths(),
  ],
+   ssr: {
+     noExternal: [
+       "@mui/joy",
+     ],
+   },
});

これで完了です!
お疲れ様でした!

まとめ

「Vite」に関するエラーと「Emotion」に関するエラーについて、まとまっているものがあまりなくて解決に時間がかかってしまいました、、
ぜひみなさんは、この記事を参考につまずかないようにしてください!

Joy UIを実際に触ってみた感想などについてまとめていきます。
よかったら他の記事も読んでください〜