Open11

fresh (deno) で shadcn/ui を使う

nikogolinikogoli

https://zenn.dev/nikogoli/scraps/207599bd096dff


モチベーション

https://ui.shadcn.com/
Radix UI と Tailwind CSS を用いたコンポーネントライブラリ shadcn/ui を fresh で使う。

この分野でよくあるように shadcn/ui (の基礎の Radix UI) は React への依存が強く、preact ベースである fresh で使うことはできない。

が、「手作業で色々書き直したら使えないことはない」的な issue コメントを見つけたので、自分でも試してみる。


デモのページ
https://testing-shadcn-ui-in-fresh.deno.dev/

https://github.com/nikogoli/testing-shadcn-ui-in-fresh

nikogolinikogoli

わりと Radix-ui / Tailwind 以外の外部パッケージの使用も多く、それはどうなのと思う。

What do you mean by not a component library?
I mean you do not install it as a dependency. It is not available or distributed via npm.
Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours.

結局 shadcn-ui の dependency を install する必要があるなら、誇大広告じゃないですかね

nikogolinikogoli

準備

  1. 上述の issue に従い、unocss が提供する preset 経由で shadcn/ui の tsx を入手する。
    ↓ の src 以下のファイルを いい感じに fresh プロジェクトに移す。
    https://github.com/fisand/unocss-preset-shadcn

  2. import map に関連ライブラリを追加する。
    自分の場合は、deno.json に以下のように追記した。
    deno.json
     {
       "imports": {
         "$fresh/": "https://deno.land/x/fresh@1.4.3/",
         "preact": "https://esm.sh/preact@10.15.1",
         "preact/": "https://esm.sh/preact@10.15.1/",
         "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1",
         "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
         "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
         "twind": "https://esm.sh/twind@0.16.19",
         "twind/": "https://esm.sh/twind@0.16.19/",
         "$std/": "https://deno.land/std@0.193.0/",
    +    "class-variance-authority": "https://esm.sh/class-variance-authority@0.7.0",
    +    "clsx": "https://esm.sh/clsx@2.0.0",
    +    "tailwind-merge": "https://esm.sh/tailwind-merge@1.14.0"
       }
     }
    

個別の tsx ファイルにおける共通処理

  1. React のインポートを preact/compat に差し替える
    piyo.tsx
    - import * as React from "react"
    + import * as React from "preact/compat"
    
    また、utils のインポートに拡張子を追加する

  2. interface XxxProps において存在しないXxxHTMLAttributesHTMLAttributesに変更し、それによって発生するプロパティの不一致を型操作によって解消する。

    例えば button.tsx の場合、sizeプロパティに Class Variance Authority が介入するが、これは preact/compat のHTMLAttributessizeと型が一致しない。なので、HTMLAttributesからsizeを取り除いてバッティングしないようにする[1]
    button.tsx の場合
     export interface ButtonProps
    - extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    + extends Omit<React.HTMLAttributes<HTMLButtonElement>, "size">,
         VariantProps<typeof buttonVariants> {}
    

  3. class=... で twind-css の入力ができるように引数を調整する
    button.tsx の場合
    const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
    -  ({ className, variant, size, ...props }, ref) => {
    +  ({ class:className, variant, size, ...props }, ref) => {
        return (
          <button
            className={cn(buttonVariants({ variant, size, className }))}
            ref={ref}
            {...props}
          />
        )
      }
    )
    
脚注
  1. 力技すぎるので問題があるかも ↩︎

nikogolinikogoli
  • radix-ui がからむとダメかも
    radix-ui がからむと、 React と preact/compat の型の不十分な互換性から any が出る。
    が、これは定義し直した型をローカルにおいてそこからインポートすれば回避できた

  • @radix-ui/xxx がインポートする@radix-ui/yyy が不適切なバージョンの preact をインポートするので render-to-string が失敗する
    import_map で @radix-ui/xxx系のインポートを全て external にし、1つずつ preact のバージョンを指定すると回避できる

  • preact/hooks で TypeError: _.__ is not a function のエラーが出て動作しない
    external=deps= で preact のインポートが重複していることが原因だった。
    もう全部 externak インポートで処理することにした

nikogolinikogoli

進捗

unocss が出しているものは、slider と tooltip 以外はちゃんと動くようになった。

import map の肥大化が凄まじい。

nikogolinikogoli

Slider の Thumbs が機能しない問題

  • Radix-ui 側の言い分:preact の問題 (issue)
  • Preact 側の言い分:React と Preact で useEffect の実行タイミングの違いが原因 (issue)

という感じで、issue は立っているものの1年以上動きがなく、放置されているのが現状。

とりあえず、最低限の機能だけでも模倣したコンポーネントで代替できないか検討する

nikogolinikogoli

「ref を明示的に設定 + useEffect として外側に処理を切り出し」によって、一応は対応できた。

上記の Preact の言い分にあるように 、オリジナルでは、 useComposedRefs() として、 ref の設定処理のなかで StateUpdater を呼ぶという処理が行われている。しかし、preact でこれを行うと state が変更されても re-render が走らないらしく、それを前提としているコンポーネントの追加/削除が行われず、上のような問題が発生する。

nikogolinikogoli

React.ReactNode の置き換え

Array<VNode> | VNode | string が一番エラーが出なさそう
普通にエラー出た。安全なのは Array<VNode | string> | VNode | string か?
<span> をかませずに Element と文字列を並列させる記述は違和感あるけど...

nikogolinikogoli

Calendar が page rendering でエラーを出す問題

Calendar コンポーネントが依存している react-day-picker に対し、preact/compat が React を完全に代替できていないことが原因っぽい (エラー詳細)。

面倒なのでCalender 系は放棄する。やっぱり外部依存のせいで問題が起きたじゃないか...