31
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Refine(Refine.dev)を半年開発で使ってみてわかったこと

Last updated at Posted at 2023-12-23

NTTテクノクロスの上原です。

この記事はNTTテクノクロスアドベントカレンダー2023年、シリーズ2、23日の記事です。今回は、話題のReact用フレームワークの一つ、Refineの紹介です。

Refineとは

Refineは、React開発用の高水準なWebアプリケーション開発フレームワークです。高水準の意味は、Next.jsやViteといったレベルよりも高く、データの表示や編集、ページ概念、データソース概念など提供する高レベルなアプリケーションロジックの記述をサポートします。
また、RefineはデータベースのCRUD操作のための画面生成機能を含んでいます。

Refineは、Refine Inc.によってMITライセンスで公開されているOSSであり、コミュニティ版と有償のエンタープライズ版があります。本記事はコミュニティ版について解説します。

Refineの適用領域

Refineは、データのCRUD操作用画面を自動生成する機能を持つなど、管理画面やダッシュボードなどのアプリを作ることに向いています。とはいえ、RefineはNext.JS,Vite, Remixなどでの標準アプリケーションの一部として動作し、認証やルーティング、フォーム入出力など多岐にわたる大規模な機能セットを持っているので用途が特に限定されているわけではありません。たとえばNext.jsで導入したときNextの全機能が使えます。なので、デザインを重視する「Webサイト」としてのWebアプリケーションを主要ターゲットとしてはいませんが、それに不都合な点があるわけではありません。

画面例

以下は、Refineの画面生成機能をつかって、動作しているRESTful Webサービスから生成したCRUD画面例です。

image.png

RESTfulのルールに準拠した(かつページネーションなどを含めてjson-serverの基盤に準拠するルールに従っている)Web APIに外部から接続し、情報を収集することによって上記のCRUD編集画面群を生成することができます。また、RESTful Webサービス以外にGraphQLなどの多くのデータプロバイダが提供されて、スキーマやデータそのものからカラム情報やデータ型を読みとって、画面を生成します。

Refineのアーキテクチャ

Refineは非常にカスタマイズ性が高いのですが、そのアーキテクチャの一部を図示すると以下のようになります。

image.png

Refineは、主要なライブラリを組合わせる、グルー的な役割りを果たすフレームワークといえます。また全体は、Next.js, Vite, RemixとCRAのいずれかで動作するWebアプリとして初期生成できます。
以下、構成について簡単に説明します。

  • アプリケーションコードは、@refindevの名前空間に所属するnpmモジュール群で提供されるフック群を使用して記述します。これらのフック群は、Tanstack Table、TanStack Query、React Hook Formなどの全機能を利用可能にしつつ、連携するように実装されています。
  • アプリケーションコードは、プロジェクトの初期構成コードの二つのジェネレータ、WebベースのScaffolderもしくはcreate refine-appを使用して初期生成します。生成されたコードを開発者が手で書き変えて開発します。
  • Refineは「データプロバイダ」と呼ぶ、外部のデータソースへの接続をラッピングする間接層をつかってデータアクセスを行います。
    • 外部データは、データプロバイダを通じて操作する「リソース」という概念で抽象化されています。Refineは、リソースを単位として扱うCRUD画面の自動生成機能(Inferencer、推論機)を持っています。
    • リソース追加は、Refine CLIを使って行います。
  • 現代的なWebアプリが具備しているリッチな機能や便利な機能をアウトオブボックスで提供している。たとえば、以下の機能が利用できます。

Refineの特徴と主要機能

以降、Refineの特徴や重要な機能について、一部ですが解説していきます。

「ヘッドレス」なフレームワーク

Refineは、自身を「ヘッドレス」だと説明しています

ヘッドレスとは、コアが特定のUIコンポーネントライブラリには依存していないということです。その上で、Mantine, Chakra UI, Ant Design, MUIなどのUIライブラリのそれぞれをターゲットとするコードジェネレータがサポートされています。これらの複数のUIライブラリから好みのものを選んで適用できます。スタイル修飾のない生成を行い、任意のCSSフレームワーク(例えばTailwindなど)でスタイルをつけることもできます。

もうちょっと言えば、Refineの方針は、「用意された、設定画面用のUIコンポーネント群を組合せれば画面がつくれる」というようにはあえてしていない、ということです。UIコンポーネントについては囲い込みをおこなわず、その使いぶりについては、Refineは侵襲的・網羅的には関知せず、せいぜいコードジェネレータでお手本コード例を示すだけです。足りなければ自分で作り込めばいいという、UIコードについて、コンポーネントライブラリの選択含めてユーザランド側に置く、オープンな立場なのです。ノーコードではなく、コードフルなコード生成アプローチだと言えます。個々のUIコンポーネントライブラリに特化したコンポーネントについては、たとえばReact-Hook-Formの機能をつかって自分でバインドしてよということです。

言い換えれば、Refineは画面構築に関して「生殺与奪の権」をRefineに明け渡す必要がないのです。アプリケーション記述の主体はRefine利用者側にあり、バインディングの責任は基本的に利用者にあり、Refineはそれを助けるというイメージです。このことは後述のプロバイダとSwizzleの仕組みについても言えます。

プロバイダ

Refineはライブラリのラッパーを「プロバイダ」と呼び、以下のような各種目的のプロバイダをそろえていて、アプリケーションコードから特定のライブラリへの直接依存を避けることができます。

  • データプロバイダ: Simple REST API, GraphQL,NestJS CRUD, Nestjs-Query, Airtable, Strapi, GraphQL, Supabase, Hasura, Appwrite, Medusa, 他にコミュニティ提供のデータプロバイダもある。
  • 認証プロバイダ: Basic, Okta, Keycloak, Auth0, Google Auth, OTP Login, Appwrite, Supabase, Strapi, Strapi, Nhost, Basic with Nextjs, Basic with Remix, Kinde
  • アクセスコントロールプロバイダ: Casbin, CASL, Cerbos and AccessControl.js
  • 通知プロバイダ: React Toastify
  • I18nプロバイダ
  • ライブプロバイダ
  • ルーティングプロバイダ
  • 監査ログプロバイダ

これら、あらかじめ用意されているプロバイダを使うこともできるし、カスタマイズが必要なとき、ライブラリのコードをユーザランドにコピーして編集することになりますが、その作業を定式化するSwizzleという仕組みがあります。この機能は概念的には脱出(Ejection)です。この機能があることも、「生殺与奪の権をRefineに明け渡さない」に通じています。

プロバイダとヘッドレスの2つの特徴によるカスタマイズ性は高く、RefineはReact Native や Electronにも対応しているとのことです。

「え、でも・・・」と思われたかたへ

「いろんなライブラリに対応するためにプロバイダという間接層を設ける」というRefineの特徴は、複雑化を招きよろしくないのではないかと思われるかもしれません。そういう面がありうることは否定しません。

しかし、Refineを理解するためのポイントは、Refineがアプリケーションレベルのフレームワークだということです。

みなさんは、ネットにあるWebアプリケーション開発を紹介する記事で、以下のようなベストプラクティスを読んだり、あるいは実践されていたりしませんでしょうか。

特定のライブラリへの依存を避けるために、いったん間接レイヤを設けることで、ライブラリの将来の変更時の修正を減らす。たとえば、直接React RouterのナビゲーションやNext.jsのuseRouter Hookを使わずに、ユーザコード側でRouterといった間接レイヤを設け、React RouterやNextへの依存はその中に閉じ込める。

数多くのライブラリが栄枯盛衰し続けることが宿命のこの業界において、この種の「防御的」なアプローチは、開発するサービスやプロダクトが、ビジネスに対応し続けて長期間のライフスパンを持つなら順当です。

そして、Refineは、そういう「誰もが書いてきた、書くしかなかった間接層」の、お手本を実際のコードとして提供していると見ることができます。どうせ大規模化したら薄いラッパーを書くのです。なら、Refineのラッパーの仕組みを使っちゃうことも、原理的には厭う必要はないのです。

この目的でのラッパーは、その下位層の存在と利用方法をRefineを使う開発者が熟知していて、かつそのラッパーが「薄い」ものであり、さらに「脱出ハッチ」として下位層アクセスも許すAPIになっていれば許容できるはずです。わたくしがRefineを半年使ってきた限りでは、Refine側はこれらの条件をおおむね満たしているようには感じました。

ドキュメントを読み解くのは大変ですけどね。そこのポリシーや構造を理解すれば、その効用、すなわち特定ライブラリへの依存を切ることの利点を享受できます。もちろん、Refineへの依存は避けられませんが、まあそういう覚悟でということです。

Inferencer

RefineのInferencerは、Refineにおけるユニークで便利な機能の一つです。InferencerはCRUD画面のジェネレータです。たとえばNext.js場合、以下の手順で利用します。

  • npx refine add resourceでリソースを追加。これによって
    • pages/_app.tsx中にリソース定義が追加される。
    • src/components/に各画面のコードが生成される。ただし、このコードは「Ineferencerの動的なモード」を提供するコンポーネントを使用しているため、CRUD操作はできるものの一時的なものと考えたほうがいい。
  • Next.jsの場合、上記のコンポーネントに修正が必要なので修正を行う(後述チュートリアルにて説明)
  • 動的なモードで動作させて、ブラウザにガイダンスされる操作に従い、静的な生成コードを得る
  • 得られた生成コードを、Inferencerの動的なモードのコードに上書きする形で、各画面のコードに張り込む。

なお、Inferencerについては以下に注意が必要です。

こちらで明記されているように、Inferencerは実験的なパッケージであり、現在開発の初期段階にあるとのことです。たしかに問題がぼちぼち起きることがあります。今のところはそれを承知で使うことになります。

正直Inferencerには2023年12月現在、ぼちぼち問題はあります。ただ結局、Inferencerは「参考となるコード」を提示するだけの機能なので、多くの場合は、生成されたコードを修正することでたいていは回避することができます。

チュートリアル

以降、Refineを使ったアプリケーション開発のチュートリアルを示していきます。

準備

json-serverを立ち上げておく

今回、外部データはREST Webサービスで接続するものとします。具体的には、simple-restデータプロバイダを使い、接続先として、json-serverを利用します。以下のように任意のディレクトリでjson-serverを立ち上げておきます。

❯ npx json-server --watch db.json -p 8080
    :
  Resources
  http://localhost:3000/posts
  http://localhost:3000/comments
  http://localhost:3000/profile

json-serverのデフォルト動作では、何も設定しない初期起動では、上記のようにposts, comments, profileのリソースが作成されています。今回はこれらのうち、posts,commentsをそのまま使用することにします。

以下のように、データが登録されていることを確認します。

❯ curl -X GET http://localhost:8080/posts
[
  {
    "id": 1,
    "title": "json-server",
    "author": "typicode"
  }
]
❯ curl -X GET http://localhost:8080/comments
[
  {
    "id": 1,
    "body": "some comment",
    "postId": 1
  }
]

Refineプロジェクト初期生成

npm create refine-appを起動し、以下の内容で選択していきます。

❯ npm create refine-app sample-refine-app
 _______________________________________
/ Low-code? No-code? How about Pro-code \
\ with Refine!                          /
 ---------------------------------------
        \   ^__^
            ■-■¬\_______
            (__)\       )\/\
                ||----w |
                ||     ||
✔ Downloaded remote source successfully.
✔ Choose a project template · refine-nextjs
✔ What would you like to name your project?: · sample-refine-app2
✔ Choose your backend service to connect: · data-provider-custom-json-rest
✔ Do you want to use a UI Framework?: · chakra
✔ Do you want to add example pages?: · no
✔ Do you need any Authentication logic?: · none
✔ Do you need i18n (Internationalization) support?: · i18n-chakra
✔ Choose a package manager: · npm
✔ Mind sharing your email? (We reach out to developers for free priority support, events, and SWAG kits. We never spam.) ·
⠹ Installing packages with npm


入力内容を表にすると以下のとおり。UIコンポーネントライブラリとしてここではChakra UIを選んでますがもちろん他のものも選ぶことができます。

質問項目 このチュートリアルでの選択
Choose a project template Next.js (refine-nextjs)
What would you like to name your project? sample-refine-app
Choose your backend service to connect: REST API (data-provider-custom-json-rest)
Do you want to use a UI Framework?: Chakra UI (chakra)
Do you want to add example pages?: No
Do you need any Authentication logic?: No
Do you need i18n (Internationalization) support?: Yes
Choose a package manager: pnpm
Mind sharing your email? (We reach out to developers for free priority support, events, and SWAG kits. We never spam.) 必要なら入力

「Choose a package manager:」のところは利用可能パッケージマネージャ(npm, yarn)を選べばよいです。
うまくいけば以下のようにプロジェクト生成が終了します。

Success! Created sample-refine-app at /Users/ueha-j2/work/202312/sample-refine-app 🚀

Start developing by:

  › cd /Users/(...)/sample-refine-app
  › pnpm dev

  › Join us at https://discord.gg/refine

起動テスト

pnpm run devでNext.JSのDev Serverが立ち上がります。

❯ pnpm run dev

> sample-refine-app@0.1.0 dev /Users/ueha-j2/work/202312/sample-refine-app
> cross-env NODE_OPTIONS=--max_old_space_size=4096 refine dev

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                                  │
│   — refine Devtools beta version is out! To install in your project, just run npm run refine devtools init. https://s.refine.dev/devtools-beta   │
│                                                                                                                                                  │
│   — Hello from refine team! Hope you enjoy! Join our Discord community to get help and discuss with other users. https://discord.gg/refine       │
│                                                                                                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

✓ refine devtools is running at port 5001

  ▲ Next.js 13.5.6
  - Local:        http://localhost:3000

 ✓ Ready in 1531ms
 ○ Compiling /[...catchAll] ...
 ✓ Compiled /[...catchAll] in 6.9s (1843 modules)
 ⚠ Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/messages/fast-refresh-reload

立ち上がったら

をブラウザで表示し、起動していることを確認しておきます。プロジェクト作成時にExampleをnoで選択し、CRUD操作のできるページはまだ存在していません。pages/index.tsxをNext.JSのページとして表示しているだけです。

image.png

このトップページは以降では不要なので、ファイルを削除しておきます。

rm pages/index.tsx

リソースの生成

json-serverが初期生成してくれていたRESTリソースから、postsとcommentsにあわせて、リソースを生成していきます。まずは、postsリソースの生成です。コマンドライン「npm run refine add resource」でリソース名をインタラクティブに入力します。

❯ npm run refine add resource

> sample-refine-app2@0.1.0 refine
> refine add resource

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                                  │
│   — refine Devtools beta version is out! To install in your project, just run npm run refine devtools init. https://s.refine.dev/devtools-beta   │
│                                                                                                                                                  │
│   — Hello from refine team! Hope you enjoy! Join our Discord community to get help and discuss with other users. https://discord.gg/refine       │
│                                                                                                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
? Resource Name posts
? Select Actions (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
❯◉ list
 ◉ create
 ◉ edit
 ◉ show
🎉 Resource (src/components/posts) generated successfully!

postsリソースに対して、list(一覧)、create(作成)、edit(編集・更新)、show(詳細表示)のうち、どのアクションのための画面を生成するかをチェックボックスで入力します。

なお、npm run refine add resourceでは、以下のようにコマンドラインでリソース名を指定すると、すべてのアクション用のページを生成するのですが、commentsリソースについてはこちらの方法で作成してみます。

❯ npm run refine add resource comments

以上より、以下の_pages/_app.tsxファイルが修正され、またそれ以外のファイルが生成されます。

├── pages
│   └── _app.tsx
├── comments
│   ├── create.tsx
│   ├── edit.tsx
│   ├── index.ts
│   ├── list.tsx
│   └── show.tsx
├── header
│   └── index.tsx
└── posts
    ├── create.tsx
    ├── edit.tsx
    ├── index.ts
    ├── list.tsx
    └── show.tsx

_app.tsxの変更点

refine add resourceの実行によって、pages/_app.tsxの以下の部分に、リソースの定義が追記されていることを確認します。

tsx

function MyApp({ Component, pageProps }: AppPropsWithLayout): JSX.Element {
  const renderComponent = () => {
    if (Component.noLayout) {
      return <Component {...pageProps} />;
    }

    return (
      <ThemedLayoutV2 Header={() => <Header sticky />}>
        <Component {...pageProps} />
      </ThemedLayoutV2>
    );
  };

  return <>
    <GitHubBanner />
    <RefineKbarProvider>
      {/* You can change the theme colors here. example: theme={RefineThemes.Magenta} */}
      <ChakraProvider theme={RefineThemes.Blue}>
        <DevtoolsProvider>
          <Refine
            routerProvider={routerProvider}
            dataProvider={dataProvider(API_URL)}
            notificationProvider={notificationProvider}
            options={{
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
              projectId: "m6NUOk-v05xe3-UagVjA",
            }}
+            resources={[{
+              name: "comments",
+              list: "/comments",
+              create: "/comments/create",
+              edit: "/comments/edit/:id",
+              show: "/comments/show/:id"
+            }, {
+              name: "posts",
+              list: "/posts",
+              create: "/posts/create",
+              edit: "/posts/edit/:id",
+              show: "/posts/show/:id"
+            }]}>
            {renderComponent()}
            <RefineKbar />
            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
          </Refine>
          <DevtoolsPanel />
        </DevtoolsProvider>
      </ChakraProvider>
    </RefineKbarProvider>
  </>;
}

リソースの定義方法についてはこちら。リソース定義を元にして、CRUD操作ページ間のナビゲーションやメニュー構築が行われます。

ファイル名の修正ほか

残念なことに、現在refine add resourceコマンドが生成した画面コンポーネントは、Next.jsのルーティングには合致しないので、以下のようにファイル名変更・移動を行います。なお、ここではpages routerを使用します。

mkdir pages/{posts,comments}mkdir pages/{posts,comments}/{edit,show}mv src/components/posts/create.tsx pages/posts/create.tsx
❯ mv src/components/posts/edit.tsx 'pages/posts/edit/[id].tsx'mv src/components/posts/show.tsx 'pages/posts/show/[id].tsx'mv src/components/posts/list.tsx pages/posts/index.tsx
❯ mv src/components/comments/create.tsx pages/comments/create.tsx
❯ mv src/components/comments/edit.tsx 'pages/comments/edit/[id].tsx'mv src/components/comments/show.tsx 'pages/comments/show/[id].tsx'mv src/components/comments/list.tsx pages/comments/index.tsx

さらに、これらのコンポーネントはpages直下のページと扱おうとしたとき、deafult exportが足りないので追記します。

diff --git a/pages/posts/create.tsx b/pages/posts/create.tsx
index 62ed92d..2a74920 100644
--- a/pages/posts/create.tsx
+++ b/pages/posts/create.tsx
@@ -4,3 +4,5 @@ import { ChakraUIInferencer } from "@refinedev/inferencer/chakra-ui";
 export const PostsCreate: React.FC<IResourceComponentsProps> = () => {
     return <ChakraUIInferencer />;
 };
+
+export default PostsCreate;
diff --git a/pages/posts/edit/[id].tsx b/pages/posts/edit/[id].tsx
index 350e5d8..6e75cd8 100644
--- a/pages/posts/edit/[id].tsx
+++ b/pages/posts/edit/[id].tsx
@@ -4,3 +4,5 @@ import { ChakraUIInferencer } from "@refinedev/inferencer/chakra-ui";
 export const PostsEdit: React.FC<IResourceComponentsProps> = () => {
     return <ChakraUIInferencer />;
 };
+
+export default PostsEdit;
\ No newline at end of file
diff --git a/pages/posts/index.tsx b/pages/posts/index.tsx
index 3691b7e..9cc5004 100644
--- a/pages/posts/index.tsx
+++ b/pages/posts/index.tsx
@@ -4,3 +4,5 @@ import { ChakraUIInferencer } from "@refinedev/inferencer/chakra-ui";
 export const PostsList: React.FC<IResourceComponentsProps> = () => {
   return <ChakraUIInferencer />;
 };
+
+export default PostsList;
diff --git a/pages/posts/show/[id].tsx b/pages/posts/show/[id].tsx
index 055ccf4..f38b2b7 100644
--- a/pages/posts/show/[id].tsx
+++ b/pages/posts/show/[id].tsx
@@ -4,3 +4,5 @@ import { ChakraUIInferencer } from "@refinedev/inferencer/chakra-ui";
 export const PostsShow: React.FC<IResourceComponentsProps> = () => {
     return <ChakraUIInferencer />;
 };
+
+export default PostsShow;

ここらへんちょっと残念なんですけどなんとかならないものか。

さらに、APIの通信を、最初に作成したjson-serverに振り向けるためにpages/_app.tsxには以下の修正を行います。

diff --git a/pages/_app.tsx b/pages/_app.tsx
index bec09f0..a7f0b49 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -17,7 +17,7 @@ import { ChakraProvider } from "@chakra-ui/react";
 import { Header } from "@components/header";
 import dataProvider from "@refinedev/simple-rest";
 
-const API_URL = "https://api.fake-rest.refine.dev";
+const API_URL = "http://localhost:8080";
 
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
   noLayout?: boolean;

これでブラウザで閲覧すると、post,commentsリソースの画面群でCRUD操作可能になっています。json-server上のレコードの編集や保存がすでに可能です。

image.png

ここで重要なのは、simple-restデータソースの場合、Inferenceでの画面生成のために、最低1つのレコードが登録されていることが必須だということです。これはsimple-restデータソース経由の場合、スキーマ情報がないため、データそのものからフィールドの型を決めているからです。

ただ、ここではまだ、画面生成はInferencerの動的なモードでおこなわれています。
これは実用のものではないので、それぞれの画面で、静的にコードを生成させます。

その方法は、画面上にガイダンスが表示されているとおりなのですが、各画面での[Show the auto-generated code]のボタンを押下して、この画面を生成するために必要な静的なコードを表示させます。

image.png

ここで、[Copy Generated Code]を押下すると、そのページを表示するのに必要なコードとしてクリップボードにコピーされるので、指示通りにtsxファイルの内容を置き換えます。たとえば、コメントの一覧表示

を表示していたら、対応するpages/comments/index.tsxの内容をクリップボードにコピーされたコードで置き換えます。ただし、先程と同じように、ページに対応するReact Componentにdefault export CommentList;の指定を追加する必要があります。(クラス名がCommentsListから単数形のCommentListに直されているので注意。Inferencerの仕様)。

import React from "react";
import {
    IResourceComponentsProps,
    GetManyResponse,
    useMany,
} from "@refinedev/core";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import {
    List,
    usePagination,
    EditButton,
    ShowButton,
} from "@refinedev/chakra-ui";
import {
    TableContainer,
    Table,
    Thead,
    Tr,
    Th,
    Tbody,
    Td,
    HStack,
    Button,
    IconButton,
    Box,
} from "@chakra-ui/react";
import { IconChevronRight, IconChevronLeft } from "@tabler/icons";

export const CommentList: React.FC<IResourceComponentsProps> = () => {
    const columns = React.useMemo<ColumnDef<any>[]>(
        () => [
            {
                id: "id",
                accessorKey: "id",
                header: "Id",
            },
            {
                id: "body",
                accessorKey: "body",
                header: "Body",
            },
            {
                id: "postId",
                header: "Post",
                accessorKey: "postId",
                cell: function render({ getValue, table }) {
                    const meta = table.options.meta as {
                        postData: GetManyResponse;
                    };

                    const post = meta.postData?.data?.find(
                        (item) => item.id == getValue<any>(),
                    );

                    return post?.title ?? "Loading...";
                },
            },
            {
                id: "actions",
                accessorKey: "id",
                header: "Actions",
                cell: function render({ getValue }) {
                    return (
                        <HStack>
                            <ShowButton
                                hideText
                                recordItemId={getValue() as string}
                            />
                            <EditButton
                                hideText
                                recordItemId={getValue() as string}
                            />
                        </HStack>
                    );
                },
            },
        ],
        [],
    );

    const {
        getHeaderGroups,
        getRowModel,
        setOptions,
        refineCore: {
            setCurrent,
            pageCount,
            current,
            tableQueryResult: { data: tableData },
        },
    } = useTable({
        columns,
    });

    const { data: postData } = useMany({
        resource: "posts",
        ids: tableData?.data?.map((item) => item?.postId) ?? [],
        queryOptions: {
            enabled: !!tableData?.data,
        },
    });

    setOptions((prev) => ({
        ...prev,
        meta: {
            ...prev.meta,
            postData,
        },
    }));

    return (
        <List>
            <TableContainer whiteSpace="pre-line">
                <Table variant="simple">
                    <Thead>
                        {getHeaderGroups().map((headerGroup) => (
                            <Tr key={headerGroup.id}>
                                {headerGroup.headers.map((header) => (
                                    <Th key={header.id}>
                                        {!header.isPlaceholder &&
                                            flexRender(
                                                header.column.columnDef.header,
                                                header.getContext(),
                                            )}
                                    </Th>
                                ))}
                            </Tr>
                        ))}
                    </Thead>
                    <Tbody>
                        {getRowModel().rows.map((row) => (
                            <Tr key={row.id}>
                                {row.getVisibleCells().map((cell) => (
                                    <Td key={cell.id}>
                                        {flexRender(
                                            cell.column.columnDef.cell,
                                            cell.getContext(),
                                        )}
                                    </Td>
                                ))}
                            </Tr>
                        ))}
                    </Tbody>
                </Table>
            </TableContainer>
            <Pagination
                current={current}
                pageCount={pageCount}
                setCurrent={setCurrent}
            />
        </List>
    );
};

type PaginationProps = {
    current: number;
    pageCount: number;
    setCurrent: (page: number) => void;
};

const Pagination: React.FC<PaginationProps> = ({
    current,
    pageCount,
    setCurrent,
}) => {
    const pagination = usePagination({
        current,
        pageCount,
    });

    return (
        <Box display="flex" justifyContent="flex-end">
            <HStack my="3" spacing="1">
                {pagination?.prev && (
                    <IconButton
                        aria-label="previous page"
                        onClick={() => setCurrent(current - 1)}
                        disabled={!pagination?.prev}
                        variant="outline"
                    >
                        <IconChevronLeft size="18" />
                    </IconButton>
                )}

                {pagination?.items.map((page) => {
                    if (typeof page === "string")
                        return <span key={page}>...</span>;

                    return (
                        <Button
                            key={page}
                            onClick={() => setCurrent(page)}
                            variant={page === current ? "solid" : "outline"}
                        >
                            {page}
                        </Button>
                    );
                })}
                {pagination?.next && (
                    <IconButton
                        aria-label="next page"
                        onClick={() => setCurrent(current + 1)}
                        variant="outline"
                    >
                        <IconChevronRight size="18" />
                    </IconButton>
                )}
            </HStack>
        </Box>
    );
};

export default CommentList;

このコードの説明は省略しますが、TanStack QueryやTanStack Tableを使用して作成されています。

あとはひたすら全ページに対してこのコードの作成と貼り込み操作を行います。
このInferencerはなかなか賢くて、配列に対応してたり、フィールド名の命名規約からテーブル間のリレーションをつくってくれたりします。

アプリケーションもしくは設定画面などの開発の実際としては、このように生成されたtsxファイルを拡張したり修正していくイメージになります。

問題点や懸念

  • SSG未対応: Next.JSのSSG(Static Site Generation)を、Refineは基本的にはサポートしていません。
    やってみると、editやshowのページ、たとえば http://loalhost/resource/edit/:id: といった処理対処のIDを含むURLで、それらをSSGしようとしてしまいます。これらのIDは、本来データベースなどから取得するランタイムでしか得られない情報なので、本質的にSSGすることはできないのです。だから、Refineの初期生成サンプルで使用されている、pages/resource/edit/[id].tsx といったページはSSGでは使用できないことになります。
    この問題は、いちおう http://loalhost/resource/edit/?id=:ID: というようにURLのQueryStringでidの値をもちまわすことで回避できますが、[create]や[delete]などのボタンの飛び先のURLが変更されることがあるので、それらのボタンを差し替版を作った上でInferencerの生成コードを修正することを含む、わりかし神経を使う対応が必要です。RefineでSSGはしなくてすむなら避けた方が良いでしょう。
  • 生成コードの修正: Inferencerは基本一回限りの利用であり、カスタマイズした後でデータ構造がかわったときなどにもうかつにInferencerで再生成してしまうわけにはいきません。できるだけ、張り込む部分に変化が置きないようなコード修正をなるべく行いたい。たとえばimport文で使用するコンポーネントと同じ名前のコンポーネントで差し替えるかたちでカスタマイズするなど。

まとめ

Refineは現代的かつ汎用的かつ包括的なReactアプリのためのWebアプリケーションフレームワークです。その機能セットの規模と包括性、柔軟性において他に類を見ない野心的なものといえるでしょう。また活発に開発されている現在進行形のOSSでもあります。

ただ、Refineを使いこなすにはある程度の学習と習熟はまちがいなく必要です。Refineは「誰でもすぐに使える」設計にはなっていません。その理由の一つは、Refineは、下位層で使用している技術をわかっていて使うことが要求されるものであるからです。たとえば、React Queryなり、Rect Hook Formsをわかってて、場合によっては自分でバインディングを書ける人にとってRefineを一番使いやすいと感じるのです。

そういう意味ではなかなか手応えがありますが、Refineの習熟は、ボイラープレートを自分で編み出して書く負担を軽減し、管理等の開発速度を爆速にしうるし、それだけではなく、その学習の意味は、ヘッドレスライブラリを使いこなす、モダンかつ継続開発を前提とした長寿命のWebアプリケーションを開発する手法をまなぶことになっているでしょう。

さらに、これは希望を含めてですが、Refineは、企業内システム、帳票系などのフロントでのReact利用起爆剤になり得るとおもっています。その理由は、ヘッドレスだから特定のUIコンポーネントライブラリに依存しておらず、各機能をプロバイダで抽象化していることで、長寿命の安定したプロダクトとなり得るからです。それは企業内システムでは必須のことです。

この記事では、今振り返って最初に読めていたら良かっただろう、自分がなかなか読みとることができなかったコアの設計や思想についての理解の助けになることを目標として執筆してみました。みなさまがRefineを取り組むとしたら、その一助になれば幸いです。

ではもうすぐのメリークリスマスに乾杯!!あしたは、@korodroidさんの記事です。

おまけ

いままでに書いてきたAdvent Calendarの記事を記録として列挙しておきます。

あと宣伝ですが、IT MediaでT3 Stackの連載を続けてます。興味あればどうぞ!!

31
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?