Yuto Blog
speaker-deck-iconyoutube-iconrss-iconx-icongithub-icon

HonoXでshadcn/uiを使用する

目次

Intro

Honoはreact-rendererというmiddlewareを提供しています。これを使用すればReactが動きます。すごい。

そして最近のUIコンポーネント系で人気なshadcn/uiというものがあります。これはReactに依存しているので先ほどのmiddlewareの出番ということですね。 最近このUIコンポーネントをHonoXで使いたいということがあり、その際に苦戦したポイントなどをまとめておきます。

Repository

https://github.com/yossydev/honox-shadcnというリポジトリを作ってあります。これを参考にしていただければ簡単にしていただければ環境作れると思います。 実際に動作見てみたい方はcloudflare pageでデプロイしてあるので、https://honox-shadcn.pages.dev/でみれます。

苦戦したところ

HonoXのREADMEにreactRendererの使い方があるんですが、そこに書いてある内容だけでは動かなかったので、いくつか自分の方で書いておきます。

vite.config.tsにssrオプションを追加

ssr: { external: ["react", "react-dom"] },

reactとreact-domをssrオプションに追加しましょう。基本htmlとcssはssrで事前に生成したものをクライアントに渡しているので、その生成時にreactとreact-domを外部パッケージとして含めてあげる必要があります。

[vite] Error when evaluating SSR module /~~~~/node_modules/.pnpm/[email protected]/node_modules/react/jsx-runtime.js:
|- ReferenceError: module is not defined

設定していないとこのようなエラーになると思います。

HasIslandが動かない

基本HonoXを使う方はScriptコンポーネントを使用しているかと思いますが、その内部にはHasIslandというコンポーネントを使って、scriptを読み込むかどうかのチェックを行なっています。

HasIslandは以下のような実装になっています

import type { FC } from 'hono/jsx'
import { useRequestContext } from 'hono/jsx-renderer'
import { IMPORTING_ISLANDS_ID } from '../../constants.js'

export const HasIslands: FC = ({ children }) => {
  const c = useRequestContext()
  return <>{c.get(IMPORTING_ISLANDS_ID) ? children : <></>}</>
}

ここで問題なのが、このコンポーネントがhono/jsx-rendererを使用しているところです。

reactRendererを使用する場合は@hono/react-rendereruseRequestContextを使う必要があるので、HasIslandコンポーネントは動かずせっかくのislandアーキテクチャが意味をなさなくなってしまいます。

import { reactRenderer } from "@hono/react-renderer";
import { useRequestContext } from "@hono/react-renderer";
import { FC, PropsWithChildren } from "react";

const HasIslands: FC<PropsWithChildren> = ({ children }) => {
  const IMPORTING_ISLANDS_ID = "__importing_islands" as const;
  const c = useRequestContext();
  return <>{c.get(IMPORTING_ISLANDS_ID) ? children : <></>}</>;
};

回避策として、自分でコンポーネントを作ればislandコンポーネントが機能するようになります。

__importing_islandsという定数は、ファイル内にislandディレクトリからのimportがあれば付く識別子です。それを使ってc.get(IMPORTING_ISLANDS_ID)とgetすることでislandコンポーネントがimportされていればtrueが返ってくるみたいな仕組みになっています。 ここら辺は以前コントリビュートした際の知見が活きています💪 feat: preserved file island support #114

この件に関してはCreating HasIslands component with useRequestContext in react-renderer #127でissueを出しました。

client.tsで型エラーになる

READMEによると、以下のようにclient.tsをするように書いてあります

import { createClient } from "honox/client";

createClient({
  hydrate: async (elem, root) => {
    const { hydrateRoot } = await import("react-dom/client");
    hydrateRoot(root, elem);
  },
  createElement: async (type: any, props: any) => {
    const { createElement } = await import("react");
    return createElement(type, props);
  },
});

しかしこれだとTSのコンパイルエラーになります

app/client.ts:6:23 - error TS2345: Argument of type 'Node' is not assignable to parameter of type 'ReactNode'.

6     hydrateRoot(root, elem);
                        ~~~~

app/client.ts:8:3 - error TS2322: Type '(type: any, props: any) => Promise<React.CElement<any, React.Component<any, any, any>>>' is not assignable to type 'CreateElement'.
  Type 'Promise<CElement<any, Component<any, any, any>>>' is not assignable to type 'Node | Promise<Node>'.
    Type 'Promise<CElement<any, Component<any, any, any>>>' is not assignable to type 'Promise<Node>'.
      Type 'ComponentElement<any, Component<any, any, any>>' is missing the following properties from type 'Node': baseURI, childNodes, firstChild, isConnected, and 46 more.

8   createElement: async (type: any, props: any) => {
    ~~~~~~~~~~~~~

この問題もすでにissueがあり既知の問題みたいなので、そのうち直されると思います(Type Error Occurs When Trying to Use React with createClient #87)

SSGするとエラーになる

TypeError: __vite_ssr_import_0__.jsxDEV is not a function

↑ のようにSSGを使うとエラーになります。これがあんまりわかってなくて、一旦SSGしないようにしているんですが、またどこかで調べてみたいです。

まとめ

いくつか苦戦ポイント上げましたが、これ以外は特になくて、基本環境構築もshadcn/ui通りに進めればできるのでとっても良いです。

関連

0