React Adminのチュートリアルをやってみた

# なぜReact Adminなのか
実務でダッシュボードアプリケーションを新規に作成する必要があり、React.jsを使用する予定なのですが、開発効率を上げるためにライブラリやフレームワークを利用することが検討されています。思いの外色々な選択肢があるのですが、その中でも人気の最も高いと思われるReact Adminを触ってみたいと思いました。

# React Adminとは
Githubリポジトリのスター数は21.4k(記事執筆時点)で、他の類似のものと比べてもダントツで認知されているようです。

https://github.com/marmelab/react-admin#features

まずはざっくりと概要をつかんでみたいと思います。公式ドキュメントは全て英語で書かれていて言語の選択はできないようです。

https://marmelab.com/react-admin/

>（原文） **The React Framework For B2B Apps**
> 
> React-admin offers the best developer experience, lets you focus on business needs, and build delightful user interfaces.
> 
> （訳文）B2BアプリのためのReactフレームワーク
> 
> React-adminは、最高の開発者体験を提供し、ビジネスニーズに焦点を当て、楽しいユーザーインターフェイスを構築することができます。
> 

B to B向けのアプリケーションのためにあり、より簡単に開発することができて、かつ、良いUIを提供することができるみたいです。DXもUXも充実しているということですね。

> （原文）A frontend Framework for building data-driven applications running in the browser, on top of REST/GraphQL APIs, using [React](https://facebook.github.io/react/) and [Material Design](https://material.io/). Open sourced and maintained by [marmelab](https://marmelab.com/).
> 
> （訳文）ReactとMaterial Designを使って、REST/GraphQL APIの上で、ブラウザ上で動作するデータ駆動型アプリケーションを構築するためのフロントエンドフレームワークです。marmelabによってオープンソース化され、メンテナンスされています。
> 
> （原文）Powered by [MUI](https://mui.com/), [react-hook-form](https://react-hook-form.com/), [react-router](https://reacttraining.com/react-router/), [react-query](https://react-query-v3.tanstack.com/), [TypeScript](https://www.typescriptlang.org/) and a few more
> 
>（訳文）他にもreact-hook-form、react-router、react-query、TypeScriptなどを使用している

使用する技術スタックはReact.js、MUI、TypeScript、React Hook Form、React Router、React Queryなどなどということで、1つのフレームワークに多くのライブラリを内包しているようです。
データ駆動型アプリケーションなので、よりデータに忠実なアプリケーションを作ることができそうです。

デモ動画を見てみると非常に多くの操作を実現することが可能で、スムーズな操作感を得ることができそうな印象でした。


# やってみる
React Adminでは公式にチュートリアルを用意していますので、百聞は一見に如かずということで実際にやってみました。
チュートリアルは、ソースコードや反映後の画像や動画が添付されているため非常に分かりやすい感じになっていますが、当然全て英語です。DeepLで翻訳しながら取り組みます。

## 環境構築
以下のコードが添付されているので順番に実行していきます。
```bash
yarn create vite test-admin --template react-ts
cd test-admin/
yarn add react-admin ra-data-json-server
yarn dev
```
vite.jsをビルドツールとして、TypeScriptを利用しReactアプリケーションを構築します。サーバーを立ち上げるときは5173番ポートで接続されます。特定のフレームワークには依存せず、create-react-appでもNext.jsやRemixの環境でも問題なく導入できるみたいです。
サーバーを立ち上げた後の初期の画面はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/199bc5f5-4da9-721b-4d4d-dfa20ec8ab5e.png)

## 各バージョン情報
- yarn：1.22.19
- React.js：18.2.0
- TypeScript：4.9.3
- React Admin：4.7.1
- ra-data-json-server：4.7.0
    - React Admin専用で、JSON PlaceholderなどのJSONサーバーを利用したREST APIに対応するデータプロバイダ
    - JSON PlaceholderのようにAPIが用意されていて、CRUD操作を試すことができる

https://www.npmjs.com/package/ra-data-json-server

package.jsonの中身はこんな感じです。
```json
{
  "name": "test-admin",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "ra-data-json-server": "^4.7.0",
    "react": "^18.2.0",
    "react-admin": "^4.7.1",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.26",
    "@types/react-dom": "^18.0.9",
    "@vitejs/plugin-react": "^3.0.0",
    "typescript": "^4.9.3",
    "vite": "^4.0.0"
  }
}
```

## データプロバイダを利用可能にする
①App.tsxを以下のように書き換えます。
```tsx
import { Admin } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com');

const App = () => <Admin dataProvider={dataProvider} />;

export default App;
```
②もう一度サーバーを立ち上げるとReact Adminが適用できていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/da55a7b5-de89-e1ed-7548-f28180e2c954.png)

### Adminコンポーネントとは
- React Adminのルートコンポーネントである
- dataProviderはAPIからデータを取得するための関数を受け取る
- React Adminを独自のAPIに接続するためにはdataProviderをカスマイズする必要がある

③index.cssでレイアウトを調整します。
```css
body {
    margin: 0;
}
```
index.htmlにRobotoフォントを追加します。
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Admin</title>
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
    />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>
```

## APIのエンドポイントとの設定
ユーザーの一覧情報を追加していきます。
①App.tsxにResourceコンポーネントを追加する
```tsx
import { Admin, Resource, ListGuesser } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com');

const App = () => (
 <Admin dataProvider={dataProvider}>
   <Resource name="users" list={ListGuesser} />
 </Admin>
);

export default App;
```

### Resourceコンポーネントとは
- Adminコンポーネントには1つ以上のResourceコンポーネントが必要である
- APIのエンドポイントに名前をマッピングする
- name
    - originの後続に続くパスとなり、そのパスから情報を取得するようにreact-adminに通知する
    - 今回で言うところの「users」
    - 各CRUD操作（リスト、作成、編集、表示）に使用するコンポーネント側でも定義する必要がある
- list
    - 一覧表示するためにListGuesserコンポーネントを使用するようにreact-adminに指示する
    - ListGuesserコンポーネントはAPIから取得したデータを基に一覧表示するカラム名や型を推測する

②サーバーを立ち上げて確認する
UI上でテーブルのヘッダー部分の各カラムをクリックすると並び替えすることができます。その度に該当するアクションの内容がクエリパラメータに渡されてデータを取得し表示します。
例：「Name」をクリックして、ユーザー名を昇順に並び替えた時のページURL
```
http://127.0.0.1:5173/#/users?filter={}&order=ASC&page=1&perPage=10&sort=name
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/ecc5b2da-38a8-4f36-4c36-59c55a13dea8.png)

## 画面をカスタマイズ可能にする
現時点で使用しているListGuesserコンポーネントは実運用で使用することを想定していないため、独自のコンポーネントに置き換える必要があります。
①ListGuesserコンポーネントがコンソールに出力するReact.jsのコードを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/6553f88a-975b-bec3-9c1e-2274156b4d4d.png)

②users.tsxを作成して出力されたコードを追加する
```tsx
import { List, Datagrid, TextField, EmailField } from "react-admin";

export const UserList = () => (
  <List>
    <Datagrid rowClick="edit">
      <TextField source="id" />
      <TextField source="name" />
      <TextField source="username" />
      <EmailField source="email" />
      <TextField source="address.street" />
      <TextField source="phone" />
      <TextField source="website" />
      <TextField source="company.name" />
    </Datagrid>
  </List>);
```

③App.tsxで、ListGuesserコンポーネントの代わりにUserListコンポーネントに変更する
```tsx
import { Admin, Resource } from "react-admin";
import jsonServerProvider from "ra-data-json-server";
import { UserList } from "./users";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");

const App = () => (
  <Admin dataProvider={dataProvider}>
    <Resource name="users" list={UserList} />
  </Admin>
);
```

④サーバーを立ち上げて確認する
変わりなく表示されていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/6bbea77e-dc49-f559-0d28-a15127ebb72f.png)

## 画面をカスタマイズする
①表示する内容を変更する
```tsx
import { List, SimpleList } from 'react-admin';

export const UserList = () => (
  <List>
    <SimpleList
      primaryText={(record) => record.name}
      secondaryText={(record) => record.username}
      tertiaryText={(record) => record.email}
    />
  </List>
);
```
### Listコンポーネントとは
- URL からクエリパラメータを読み取り、パラメータに基づいて API 呼び出しを作成する
- 結果を Reactコンテキストに格納する
- 子コンポーネントがリストのフィルタ、ページ付け、ソートを変更できるように、一連のコールバックを構築する
- データ取得の部分のみを行い、実際のレンダリングは子コンポーネントに委ねる
- 子コンポーネントは自由にカスタマイズできる（例ではSsampleListコンポーネントに変更している）

②サーバーを立ち上げて確認する
名前、ユーザー名、メールアドレスだけの表示に変更されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/cf7e0a49-ed97-6cde-c1d1-a303f584166a.png)

## レスポンシブ対応する
React Adminでは標準でレスポンシブ対応しているので、何もしなくても以下のように表示されます。
例：横幅375px
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/d4e00850-f944-b1c3-3cc8-0f47ed7e4ae4.png)

MUIのuseMediaQueryを利用して、デスクトップ端末の時はカラム数を多くして、モバイル端末の時はカラム数を減らします。
```tsx
import { useMediaQuery } from "@mui/material";
import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin";

export const UserList = () => {
  const isSmall = useMediaQuery((theme) => theme.breakpoints.down("sm"));
  return (
    <List>
      {isSmall ? (
        <SimpleList
          primaryText={(record) => record.name}
          secondaryText={(record) => record.username}
          tertiaryText={(record) => record.email}
        />
      ) : (
        <Datagrid rowClick="edit">
          <TextField source="id" />
          <TextField source="name" />
          <TextField source="username" />
          <EmailField source="email" />
          <TextField source="address.street" />
          <TextField source="phone" />
          <TextField source="website" />
          <TextField source="company.name" />
        </Datagrid>
      )}
    </List>
  );
};
```
公式に説明はありませんが、breakpointsを参照するために、App.tsxに以下を追加してMUIのデフォルトのテーマとReact Adminのデフォルトのテーマを結合して対応しました。
```tsx
// react-adminとMUIのテーマを結合
const muiDefaultTheme = createTheme()
export const theme = { ...defaultTheme, ...muiDefaultTheme }
```
users.tsxにも以下のように変更しました。
```tsx
import { useMediaQuery } from "@mui/material";
import { Datagrid, EmailField, List, SimpleList, TextField } from 'react-admin';
import { theme } from "./App";

export const UserList = () => {
  const isSmall = useMediaQuery(theme.breakpoints.down("sm"));
```

https://mui.com/material-ui/react-use-media-query/

https://mui.com/material-ui/customization/theming/

https://mui.com/material-ui/customization/default-theme/?expand-path=$.breakpoints.values


## 各フィールドの表示をカスタマイズする
前述したように、Listコンポーネントはデータの取得のみを担います。そのことにより子コンポーネントは表示にのみに集中することができます。子コンポーネントをカスタマイズして、フィールドの表示を変更してみます。
Listコンポーネントのうちデスクトップ端末向けには以下のように実装しています。
```tsx
<Datagrid rowClick="edit">
    <TextField source="id" />
    <TextField source="name" />
    <TextField source="username" />
    <EmailField source="email" />
    <TextField source="address.street" />
    <TextField source="phone" />
    <TextField source="website" />
    <TextField source="company.name" />
</Datagrid>
```
DatagridコンポーネントがField系コンポーネントを囲んでいます。DatagridコンポーネントとField系コンポーネントの違いは以下です。

### Datagridコンポーネントとは
- 各データに対して１行のテーブルを作成する
- Field系コンポーネント（TextFieldやEmailField）によってカラムを決めている
- Field系コンポーネントを選択することで、必要なカラムだけ表示するようにカスタマイズすることができる
- 各行に対してRecordContextを作成して現在のレコードを保存する

### Field系コンポーネントとは
- source
    - APIレスポンスのそれぞれのフィールド名を指定する
- 様々なコンポーネントが存在し、データの値の種類（数値、日付、画像、HTML、配列、リレーションシップなど）によって最適なコンポーネントを選択することができる

Field系コンポーネントをカスタマイズして組み合わせることでカスタマイズの幅が広がりそうです。実際にFieldコンポーネントは自作することができます。自作するには作成するコンポーネントにデータを読み取らせる必要があります。具体的にはuseRecordContextを使用します。

### useRecordContextとは

- 現在のレコードを取得する
- 一般的にはカスタムフィールドを作成するときに使用する
- レコードは未定義である可能性があるため、以下のような例外処理を行う
```tsx
if (!record) return null;
```

具体的には以下のように自作することができます。
```tsx
import { useRecordContext } from "react-admin";

type Props = {
  source: string;
}

const MyUrlField = ({ source }: Props) => {
  const record = useRecordContext();
  if (!record) return null;
  return <a href={record[source]}>{record[source]}</a>;
};

export default MyUrlField;
```

## スタイルをカスタマイズする
React AdminはMUIに依存しています。MUIではsxというpropsを使用して、CSS in JSのようにスタイルをカスタマイズしていくことができます。他にもMUIではStyled componentsなどのサポートも行なっているようです。
自作したMyUrlFieldコンポーネントのスタイルをカスタマイズします。
```tsx
import { useRecordContext } from "react-admin";
import { Link } from "@mui/material";
import LaunchIcon from "@mui/icons-material/Launch";

const MyUrlField = ({ source }) => {
  const record = useRecordContext();
  return record ? (
    <Link href={record[source]} sx={{ textDecoration: "none" }}>
      {record[source]}
      <LaunchIcon sx={{ fontSize: 15, ml: 1 }} />
    </Link>) : null;
};

export default MyUrlField;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/4a82d07a-a227-758f-32d1-7d128328d91c.png)

## リレーションシップを利用する
React Adminは前述したように「データ駆動型アプリケーションを構築するためのフロントエンドフレームワーク」です。そのため、データベースのリレーションを利用することができます。ここが他に見ない大きな特徴であると感じました。

例えば、JSON Placeholderでは各投稿にuserIdが存在します。
```json
{
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
    "userId": 1
}
```
React Adminではデータへの参照を取得するために、userIdのような外部キーを利用することができます。

①App.tsxで、`/posts`のAPI用にResourceコンポーネントを作成する
```tsx
const App = () => (
  <Admin dataProvider={dataProvider}>
    <Resource name="users" list={UserList} />
    <Resource name="posts" list={ListGuesser} />
  </Admin>
);

export default App;
```

②ListGuesserコンポーネントによりコンソールに以下が出力される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/65cf474a-fd31-dcd5-a14e-b0856b7a2b6b.png)

③コンソールでの提案に従い、posts.tsxにPostListコンポーネントを作成する
```tsx
import { List, Datagrid, TextField, ReferenceField } from "react-admin";

export const PostList = () => (
  <List>
    <Datagrid rowClick="edit">
      <ReferenceField source="userId" reference="users" />
      <TextField source="id" />
      <TextField source="title" />
      <TextField source="body" />
    </Datagrid>
  </List>
);
```

④App.tsxにPostListコンポーネントを追加する
```tsx
const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="posts" list={PostList} />
        <Resource name="users" list={UserList} />
    </Admin>
);
```

⑤一列目にユーザーのidが表示されるが、代わりにユーザー名が表示されるように変更する
```tsx
const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="posts" list={PostList} />
        <Resource name="users" list={UserList} recordRepresentation="name" />
    </Admin>
);
```

⑥サーバーを立ち上げて確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/b151c6bc-e8d4-a0c8-b545-0d95fea1de3c.png)

データベースのリレーションシップを利用するにはReferenceFieldコンポーネントを使用すると良さそうです。

### ReferenceFieldコンポーネントとは
- 参照データを取得して、その結果でRecordContextを作成しレコードを表示する
- その際に、recordRepresentationをレンダリングする
- recordRepresentationはResourceコンポーネントのpropsであり、ここにカラム名を指定するとそのカラムを表示することができる

https://marmelab.com/react-admin/ReferenceField.html

⑦ posts.tsxで、１列目をidとして、投稿の詳細は削除し、編集ボタンを追加する
```tsx
import { List, Datagrid, TextField, ReferenceField, EditButton } from "react-admin";

export const PostList = () => (
  <List>
    <Datagrid>
      <TextField source="id" />
      <ReferenceField source="userId" reference="users" />
      <TextField source="title" />
      <EditButton />
    </Datagrid>
  </List>
);
```

⑧サーバーを立ち上げて確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/e8f1d23b-b4b1-1b8a-2466-941bbf59bf20.png)

表に表示するフィールドについて以下のように言及されています。
> （原文）From a UX point of view, fields containing large chunks of text should not appear in a Datagrid, only in detail views.
> 
> （訳文）UXの観点からは、大きなテキストの塊を含むフィールドはデータグリッドに表示されるべきではなく、詳細ビューにのみ表示されるべきです
>
ここまで丁寧に言及してくれるのはありがたいです。表示する必要のあるデータの選択と、その見せ方を工夫することが大切ですね。

## 編集機能を追加する
管理画面では当然レコード（データ）を編集できるようにする必要があります。React AdminではEditコンポーネントが用意されいて、標準で編集機能を簡単に実装できるようになっています。

### Editコンポーネントとは
- React Adminにある編集機能を備えるコンポーネントである
- レコードを取得してページタイトルを表示する

①App.tsxで、EditGuesserコンポーネントの使用を追加する
```tsx
const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="posts" list={PostList} edit={EditGuesser} />
        <Resource name="users" list={UserList} recordRepresentation="name" />
    </Admin>
);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/2e18b846-1009-a5c6-81b0-6fbe6510b91b.png)

この編集ページではフォームが機能していて、投稿時にPUTリクエストを発行します。そのため、たったこれだけの記述で編集機能を実装することができるようになっています。また、「DELETE」ボタンからDELETEリクエストを発行して削除することもできます。

②コンソールに出力されるコードを参照する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/07924fbc-312c-59b2-958f-b381dc3c21f3.png)

③posts.tsxにPostEditコンポーネントを追加する
```tsx
export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <ReferenceInput source="userId" reference="users" />
      <TextInput source="id" />
      <TextInput source="title" />
      <TextInput source="body" />
    </SimpleForm>
  </Edit>
);
```

④EditGuesserコンポーネントの代わりにPostEditコンポーネントを使用する
```tsx
const App = () => {
  return (
    <Admin dataProvider={dataProvider}>
      <Resource name="users" list={UserList} recordRepresentation="name" />
      <Resource name="posts" list={PostList} edit={PostEdit} />
    </Admin>
  );
};

export default App;
```

⑤主キー（id）の編集を無効化して先頭に表示するようにして、投稿本文にはtextareaを使用するように変更する
```tsx
export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <TextInput source="id" disabled />
      <ReferenceInput source="userId" reference="users" />
      <TextInput source="title" />
      <TextInput source="body" multiline rows={5} />
    </SimpleForm>
  </Edit>
);
```

### SimpleFormコンポーネントとは
- レコードを受け取る
- フォームのレイアウト、デフォルト値、バリデーションを担う
- フォームの入力は子コンポーネントに委ねる

### ReferenceInputコンポーネントとは
- 現在のレコードに関連する可能性のある参照をAPIで取得する
- 可能性のある選択肢を含むコンテキストを作成する
- 選択肢を表示してユーザーに1つを選択させるAutocompleteInputコンポーネントをレンダリングする

⑥サーバーを立ち上げて確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/e4bc8e0a-9a4c-777f-2a6e-23dc30dc43d0.png)

## 投稿機能を作成する
一覧機能、編集機能、削除機能を作成したので、残す投稿機能も作成します。

①posts.tsxで、PostCreateコンポーネントを作成する
```tsx
export const PostCreate = () => (
  <Create>
    <SimpleForm>
      <TextInput source="id" disabled />
      <ReferenceInput source="userId" reference="users" />
      <TextInput source="title" />
      <TextInput source="body" multiline rows={5} />
    </SimpleForm>
  </Create>
);
```

②App.tsxで、`name="posts"`であるResourceコンポーネントに`create={PostCreate}`を追加する
```tsx
const App = () => {
  return (
    <Admin dataProvider={dataProvider}>
      <Resource name="users" list={UserList} recordRepresentation="name" />
      <Resource name="posts" list={PostList} edit={PostEdit} create={PostCreate} />
    </Admin>
  );
};

export default App;
```

③サーバーを立ち上げて確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/b59e05a8-8d40-117a-4b4e-68caaa3d790b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/9e879553-34e2-d791-1829-863d6faee8fa.png)

以上の記述を追加するだけで、自動的に投稿一覧画面の上に「create」ボタンを追加して、新規投稿画面へのアクセスを可能になっています。もちろん投稿時にはAPIへPOSTリクエストを送信します。
これで一通りCRUD操作ができるようになりました。

## レンダリング最適化とUndo機能
JSON Placeholderでは実際には投稿の作成や編集は行われないが、新しく作成した投稿は投稿一覧に表示されるようになっています。React Adminはサーバーに更新用のクエリを送信する前に更新されたデータを表示するようになっています。サーバーの応答を待つことなくUIの変更があるため、非常に素早く変更を表示することができて、UXが向上します。
また、「元に戻す」機能も使え使うことができます。React Adminでは、いわゆるUndo機能が標準で備わっていて、編集後に確認のポップアップにある「Undo」を押すとサーバーに更新用のクエリを送信する前に変更前のデータを戻すことができます。

## ページタイトルをカスタマイズする
投稿編集画面では投稿のidをタイトル（ヘッダー部分）に使用していますが、これを投稿のタイトルに変更します。

そのために、posts.tsxで、PostTitleコンポーネントを作成して、Editコンポーネントのtitleに指定します。
- PostTitleコンポーネントでは、useRecordContextでレコードを取得して、タイトルカラムの値を表示するようにする
- Editコンポーネントがレコードを取得している間、PostTitleコンポーネントはレコードなしになる可能性がある
- 使用する前にuseRecordContextによって返されるレコードが定義されているかどうか確認する必要がある
```tsx
const PostTitle = () => {
  const record = useRecordContext();
  return <span>Post {record ? `"${record.title}"` : ''}</span>;
};

export const PostEdit = () => (
  <Edit title={<PostTitle />}>
    <SimpleForm>
      <TextInput source="id" disabled />
      <ReferenceInput source="userId" reference="users" />
      <TextInput source="title" />
      <TextInput source="body" multiline rows={5} />
    </SimpleForm>
  </Edit>
);
```

## 検索機能と絞り込み機能
posts.tsxで、以下のように記述することで検索機能と絞り込み機能を作成することができるようです。ここはなぜこのように記述するのかよく分かリませんでした。

```tsx
const postFilters = [
  <TextInput source="q" label="Search" alwaysOn />,
  <ReferenceInput source="userId" label="User" reference="users" />,
];

export const PostList = () => (
  <List filters={postFilters}>
    <Datagrid>
      <TextField source="id" />
      <ReferenceField source="userId" reference="users" />
      <TextField source="title" />
      <EditButton />
    </Datagrid>
  </List>
);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/d0b24822-87bb-272a-34e6-63873f702d51.png)

## メニューアイコンをカスタマイズする
Resourceコンポーネントのiconに指定することでメニューのアイコンをカスタマイズすることができます。

```tsx
const App = () => {
  return (
    <Admin dataProvider={dataProvider}>
      <Resource name="users" list={UserList} icon={UserIcon} recordRepresentation="name" />
      <Resource name="posts" list={PostList} edit={PostEdit} create={PostCreate} icon={PostIcon} />
    </Admin>
  );
};

export default App;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/94713ac5-3060-617c-9ecc-ae029138c3d0.png)

## ホーム画面をカスタマイズする
React Adminでは、デフォルトでは最初のResourceコンポーネントでレンダリングされる一覧画面をホーム画面とします。ホーム画面は、Adminコンポーネントのdashboardにコンポーネントを渡すことでカスタマイズできます。

①Dashboard.tsxを作成する
```tsx
import { Card, CardContent, CardHeader } from "@mui/material";

export const Dashboard = () => (
  <Card>
    <CardHeader title="Welcome to the administration" />
    <CardContent>Lorem ipsum sic dolor amet...</CardContent>
  </Card>
);
```

②App.tsxで、Adminコンポーネントのdashboardに渡す
```tsx
const App = () => {
  return (
    <Admin dataProvider={dataProvider} dashboard={Dashboard}>
      <Resource name="users" list={UserList} icon={UserIcon} recordRepresentation="name" />
      <Resource name="posts" list={PostList} edit={PostEdit} create={PostCreate} icon={PostIcon} />
    </Admin>
  );
};
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646829/e27ffe53-3c43-0db5-e463-d341cb3ad1b0.png)

## ログイン画面を追加する
React Adminは画面を表示する前にユーザーの認証情報をチェックして、APIが403を返した時にログイン画面にリダイレクトさせることができます。
実装するには、authProviderオブジェクトを使用して適切な場所に認証の処理を追加する機能を提供する必要があります。
チュートリアルでは認証APIを利用しないで、全てのログインリクエストを承認して、ユーザー名をlocalStorageに保存するような認証プロバイダを使用します。このようにすることで、画面遷移する度にlocalStorageにユーザー名があることが必要になります。

具体的には以下のように実装します。authProviderオブジェクトは5つのメソッドを公開する必要で、それぞれがPromiseを返します。

```ts
export const authProvider = {
  // ログイン機能
  login: (username: string) => {
    localStorage.setItem("username", username);
    return Promise.resolve();
  },
  // ログアウト機能
  logout: () => {
    localStorage.removeItem("username");
    return Promise.resolve();
  },
  // APIのレスポンスがエラーであった時の処理
  checkError: (status: number) => {
    if (status === 401 || status === 403) {
      localStorage.removeItem("username");
      return Promise.reject();
    }
    return Promise.resolve();
  },
  // ページ遷移した時の認証チェック機能
  checkAuth: () => {
    return localStorage.getItem("username")
      ? Promise.resolve()
      : Promise.reject();
  },
  // ページ遷移した時の権限とロールのチェック機能
  getPermissions: () => Promise.resolve(),
};
```

## APIとの接続について
実際のプロジェクトでは、独自のAPIを利用することになると思います。React Adminでは、データのクエリパラメータを全てdataProviderオブジェクトに委ねていて、APIのアダプタとして動作させます。任意のAPIをマッピングしたり、複数のドメインからエンドポイントを使用したりすることができます。このdataProviderをカスタマイズする必要があると思います。

### dataProviderとは
- 以下のメソッドがある
    - getList
    - getOne
    - getMany
    - getManyReference
    - create
    - update
    - updateMany
    - delete
    - deleteMany
- HTTPリクエスト発行して、レスポンスをReact Adminで利用可能な形式に変換する

チュートリアルでは以下のAPIの想定で、サンプルコードが記載されていました。
ここもしっかりとした理解ができなかったため、ドキュメントを深く読む必要がありそうです。
| Action | Expected API request |
| --- | --- |
| Get list | GET `http://my.api.url/posts?sort=["title","ASC"]&range=[0, 24]&filter={"title":"bar"}` |
| Get one record | GET `http://my.api.url/posts/123` |
| Get several records | GET `http://my.api.url/posts?filter={"id":[123,456,789]}` |
| Get related records | GET `http://my.api.url/posts?filter={"author_id":345}` |
| Create a record | POST `http://my.api.url/posts` |
| Update a record | PUT `http://my.api.url/posts/123` |
| Update records | PUT `http://my.api.url/posts?filter={"id":[123,124,125]}` |
| Delete a record | DELETE `http://my.api.url/posts/123` |
| Delete records | DELETE `http://my.api.url/posts?filter={"id":[123,124,125]}` |

```ts
import { fetchUtils } from "react-admin";
import { stringify } from "query-string";

const apiUrl = 'https://my.api.com/';
const httpClient = fetchUtils.fetchJson;

export const dataProvider= {
    getList: (resource, params) => {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
            filter: JSON.stringify(params.filter),
        };
        const url = `${apiUrl}/${resource}?${stringify(query)}`;

        return httpClient(url).then(({ headers, json }) => ({
            data: json,
            total: parseInt(headers.get('content-range').split('/').pop(), 10),
        }));
    },

    getOne: (resource, params) =>
        httpClient(`${apiUrl}/${resource}/${params.id}`).then(({ json }) => ({
            data: json,
        })),

    getMany: (resource, params) => {
        const query = {
            filter: JSON.stringify({ id: params.ids }),
        };
        const url = `${apiUrl}/${resource}?${stringify(query)}`;
        return httpClient(url).then(({ json }) => ({ data: json }));
    },

    getManyReference: (resource, params) => {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
            filter: JSON.stringify({
                ...params.filter,
                [params.target]: params.id,
            }),
        };
        const url = `${apiUrl}/${resource}?${stringify(query)}`;

        return httpClient(url).then(({ headers, json }) => ({
            data: json,
            total: parseInt(headers.get('content-range').split('/').pop(), 10),
        }));
    },

    update: (resource, params) =>
        httpClient(`${apiUrl}/${resource}/${params.id}`, {
            method: 'PUT',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({ data: json })),

    updateMany: (resource, params) => {
        const query = {
            filter: JSON.stringify({ id: params.ids}),
        };
        return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
            method: 'PUT',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({ data: json }));
    },

    create: (resource, params) =>
        httpClient(`${apiUrl}/${resource}`, {
            method: 'POST',
            body: JSON.stringify(params.data),
        }).then(({ json }) => ({
            data: { ...params.data, id: json.id },
        })),

    delete: (resource, params) =>
        httpClient(`${apiUrl}/${resource}/${params.id}`, {
            method: 'DELETE',
        }).then(({ json }) => ({ data: json })),

    deleteMany: (resource, params) => {
        const query = {
            filter: JSON.stringify({ id: params.ids}),
        };
        return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
            method: 'DELETE',
        }).then(({ json }) => ({ data: json }));
    }
};
```

## まとめ
チュートリアルのまとめに記載されていたことをピックアップしてまとめてみました。

- React-admin はカスタマイズを念頭に置いて作られた
- 主要なドキュメントを読むと良い
    - [Data Provider and API Calls](https://marmelab.com/react-admin/DataProviderIntroduction.html)
    - [Auth Provider and Security](https://marmelab.com/react-admin/Authentication.html)
    - [List Page](https://marmelab.com/react-admin/ListTutorial.html)
    - [Creation & Edition Pages](https://marmelab.com/react-admin/EditTutorial.html)
    - [Show Pages](https://marmelab.com/react-admin/ShowTutorial.html)
    - [Fields](https://marmelab.com/react-admin/Fields.html)
    - [Inputs](https://marmelab.com/react-admin/Inputs.html)
    - [The Store](https://marmelab.com/react-admin/Store.html)
- デモを活用して理解を進める
- デモでは単純なCRUDアプリケーションを超えるためのベストプラクティスを示している

# チュートリアルを終えて
チュートリアルを最後までやってみて、色々と分かったこと、感じたことを以下に簡単に書きます。

- MUIの理解が必須である

https://mui.com/material-ui/getting-started/overview/

https://zenn.dev/yumemi_inc/articles/2021-09-26-material-ui-v5
- 所々分からないところがあるので、ドキュメントを読み込む必要がある
- dataProviderやauthProviderのカスタマイズについて特に学ぶ必要がある
- 独特な仕組みであるため、ディレクトリ構成注意が必要と思う
- React Adminはカスタマイズ性が強調されていた
- カスタマイズする時には以下のフローが良いと思われる
1. Guesserを使って実装する
2. コンソールに出力された構成を参照する
3. カスタマイズしていく

チュートリアルの冒頭には「The 30 minutes tutorial」と書かれていますが、メモしながら翻訳しながら解釈しながらだったので、3時間くらいかかりました。内容としては、画像や動画、ソースコードが添付してあって非常にやりやすい内容だと思います。
もっとドキュメントを読み込む必要性を感じますが、MUIに限らず色々なライブラリを内包しているようなので、実際に開発するにあたっては他のライブラリの知識もある程度必要になってくると思いますので、そのあたりのキャッチアップもしていきたいです。
これから実際に現場で利用するかは分かりませんが、非常におもしろく便利なフレームワークであると思いますので、使ってみたいと思いますし、提案していきたいとも思います。
非常に長くなった記事を読んでいただいてありがとうございました。React Adminを使い始めたい人の参考になると幸いです。
以下に今回作成したソースコードのリポジトリのリンクを載せておきますので参考までに。

https://github.com/washogo/react-admin-tutorial-app

# 参考一覧

https://github.com/marmelab/react-admin#features

https://marmelab.com/react-admin/

https://www.npmjs.com/package/ra-data-json-server

https://mui.com/material-ui/react-use-media-query/

https://mui.com/material-ui/customization/theming/

https://mui.com/material-ui/customization/default-theme/?expand-path=$.breakpoints.values

https://mui.com/material-ui/getting-started/overview/

https://zenn.dev/yumemi_inc/articles/2021-09-26-material-ui-v5