React-AdminでCRUDを行う｜React-Admin | dataProvider

## 前提

- こちらのページでReact-Adminの全体像や各機能の解説をしていますので、ぜひそちらもご覧ください（https://qiita.com/shino_gono/items/22397f340bfb2038587d）
- この記事ではReact-Adminの導入は済んでいる前提で話を進めます。
- ドキュメントのCRUD周りが書かれているページ
- https://marmelab.com/react-admin/DataProviders.html

## React-AdminでのCRUDについて

- React-AdminのCRUDは基本的に、APIの仕様もReact-Adminに合わせられていることを前提としています。
- そのため、React-Admin導入時にはAPI側をReact-Adminに合わせて設計できるかをあらかじめ確認することを強く推奨します。
- それができない場合はReact-Adminのデフォルトの挙動を押さえて、一部処理を独自実装する必要が出てきますが、あまり簡単にできるものではないです。
- React-AdminでのCRUD操作は基本的にdataProviderというプロバイダーを作成して実装を行います。
- ほかの機能と同様に、まずはApp.jsでAdminコンポーネントに追加してあげましょう。

```jsx
// in src/App.ts
import dataProvider from './dataProvider';

export default function App() {
  return (
    <Admin dataProvider={dataProvider}>
        ...
    </Admin>
  );
}
```

- これで実装が可能になります。

## dataProviderを実装する

- 個人的にdataProviderは下記のようなディレクトリにすることを推奨します。

```jsx
 📂dataProvider
 ┗ 📜index.ts
 ┗ 📜postDataProvider.ts
 ┗ 📜userDataProvider.ts
 ┗ その他のdataProvider達
```

- 管理する情報が複数ある場合は、このようにしてdataProviderを分離させるのが管理しやすいです。

### index.ts

- index.tsは下記のようにします。

```jsx
import {
  CreateParams,
  CreateResult,
  DataProvider,
  DeleteManyParams,
  DeleteManyResult,
  DeleteParams,
  DeleteResult,
  GetListParams,
  GetListResult,
  Identifier,
  RaRecord,
  UpdateParams,
  UpdateResult,
  GetManyParams,
  GetManyResult,
  GetManyReferenceParams,
  GetManyReferenceResult,
  UpdateManyParams,
  UpdateManyResult,
} from "react-admin";
import userDataProvider from "./userDataProvider";
import postDataProvider from "./postDataProvider";

// dataProviderを作成したらここに追加する
const providers: Record<string, DataProvider> = {
  post: postDataProvider,
  user: userDataProvider,
};

const getProvider = (resource: string) => {
  return providers[resource] 
};

const dataProvider: DataProvider = {
  getList: async <RecordType extends RaRecord = any>(
    resource: string,
    params: GetListParams
  ): Promise<GetListResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.getList(resource, params);
  },

  getNameList: async (
    resource: string
  ): Promise<{ id: number; name: string }[]> => {
    const provider = getProvider(resource);
    return provider.getNameList();
  },

  getOne: async <RecordType extends RaRecord = any>(
    resource: string,
    params: any
  ) => {
    const provider = getProvider(resource);
    return provider.getOne(resource, params);
  },

  create: async <
    RecordType extends Omit<RaRecord, "id"> = any,
    ResultRecordType extends RaRecord = RecordType & { id: Identifier }
  >(
    resource: string,
    params: CreateParams<RecordType>
  ): Promise<CreateResult<ResultRecordType>> => {
    const provider = getProvider(resource);
    return provider.create(resource, params);
  },

  update: async <RecordType extends RaRecord = any>(
    resource: string,
    params: UpdateParams<RecordType>
  ): Promise<UpdateResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.update(resource, params);
  },

  delete: async <RecordType extends RaRecord = any>(
    resource: string,
    params: DeleteParams<RecordType>
  ): Promise<DeleteResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.delete(resource, params);
  },

  getMany: async <RecordType extends RaRecord = any>(
    resource: string,
    params: GetManyParams
  ): Promise<GetManyResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.getMany(resource, params);
  },

  getManyReference: async <RecordType extends RaRecord = any>(
    resource: string,
    params: GetManyReferenceParams
  ): Promise<GetManyReferenceResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.getManyReference(resource, params);
  },

  updateMany: async <RecordType extends RaRecord = any>(
    resource: string,
    params: UpdateManyParams<RecordType>
  ): Promise<UpdateManyResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.updateMany(resource, params);
  },

  deleteMany: async <RecordType extends RaRecord = any>(
    resource: string,
    params: DeleteManyParams<RecordType>
  ): Promise<DeleteManyResult<RecordType>> => {
    const provider = getProvider(resource);
    return provider.deleteMany(resource, params);
  },
};

export default dataProvider;

```

- dataProviderには下記のメソッドがすべてが必要で、使わないものがあっても定義する必要があります。
- Typescriptで実装するときは、それぞれのメソッド用に型が用意されているので、それを適応させます。
- dataProviderが呼び出される際には必ず、引数としてresource,paramsが渡されます。
- このresourceの値を見て、扱うdataProviderに処理を振り分けるようなイメージです。

## 各リソース用のdataProvider

### getListメソッド

- 一覧情報を取得するメソッドで、一覧画面を表示した際に使用されます。
- 実装例はこんな感じ

```jsx
 getList: async <RecordType extends RaRecord = post>(
    resource: string,
    params: GetListParams
  ): Promise<GetListResult<RecordType>> => {
    const { page, perPage } = params.pagination;
    const { field, order } = params.sort;
    const filter = params.filter || {};

    const response = await client.post.list.query({
      perPage: perPage,
      page: page,
      field: field,
      order: order.toLowerCase(),
    });

    const { list, count } = response as postListResponse;

    return {
      data: list as RecordType[],
      total: tatal,
    };
  },
```

- React-Adminでは、一覧取得時のAPIの戻り値はdataとtotalが必要になります。
- dataは一覧情報が格納されたjsonで、totalはその件数になります。
- totalが存在しないとデフォルトのページネーションが機能しません。これを独自実装する際には踏み倒さないといけないデフォルトの要素が多いので、API側でtotalは必ず渡しましょう
- また、dataの中身があったとしても、totalの値が0になっていると、React-Adminは一覧を表示しません。その点も注意してください

### getOneメソッド

- 一件取得のメソッドで、詳細情報を表示する際などに呼び出されます。
- 実装例はこんな感じ

```jsx
getOne: async (resource, params) => {
    // idをnumberに変換
    const id = Number(params.id);

    if (isNaN(id)) {
      return Promise.reject();
    }

    const response = await client.post.getById.query({ id: id });
    return { data: response };
  },
```

### createメソッド

- 新規作成時のメソッドで、新規作成画面から登録を実行した際に呼び出されます。
- また、CSVインポート機能などを使う際にも呼び出されます。
- 実装例はこんな感じ

```jsx
create: async <
    RecordType extends Omit<RaRecord, "id"> = any,
    ResultRecordType extends RaRecord = RecordType & { id: Identifier }
  >(
    resource: string,
    params: CreateParams<RecordType>
  ): Promise<CreateResult<ResultRecordType>> => {
    const postData = params.data as Partial<post>;

    const response: post = await client.post.created.mutate(
      postData as Required<typeof postData>
    );

    return { data: response as ResultRecordType };
  },

```

### updateメソッド

- 更新時のメソッドで、編集画面から登録を実行した際に呼び出されます。
- 実装例はこんな感じ

```jsx
update: async <RecordType extends RaRecord = post>(
    resource: string,
    params: UpdateParams<RecordType>
  ): Promise<UpdateResult<RecordType>> => {
    const id = Number(params.id);

    if (isNaN(id)) {
      return Promise.reject();
    }

    const response = await client.post.update.mutate(params.data);

    return { data: response };
  },
```

### deleteメソッド

- 削除時のメソッドで、各画面から削除を実行した際に呼び出されます。
- 実装例はこんな感じ

```jsx
  delete: async <RecordType extends RaRecord = post>(
    resource: string,
    params: DeleteParams<RecordType>
  ): Promise<DeleteResult<RecordType>> => {
    await client.post.delete.mutate({ id: Number(params.id) });

    return { data: params.previousData as RecordType };
  },
```

## Tips

### 複数削除や複数登録がAPIでサポートされていない場合

- 下記のような感じでPromise.allでmapさせちゃう方法もあります。

```jsx
  deleteMany: async <RecordType extends RaRecord = post>(
    resource: string,
    params: DeleteManyParams<RecordType>
  ): Promise<DeleteManyResult<RecordType>> => {
    await Promise.all(
      params.ids.map((id) => client.post.delete.mutate({ id: Number(id) }))
    );

    return { data: params.ids as Identifier[] };
  },
```

### 使わないメソッド

- dataProviderではすべてのメソッドを定義しておかないとエラーが出ます。
- そのため、使わないメソッドも定義することになりますが、変なものを入れておくと誤操作につながります。
- そこで、空のデータを返すような形で定義すると安全です。

```jsx
// 例：
  getMany: async <RecordType extends RaRecord = post>(
    resource: string,
    params: GetManyParams
  ): Promise<GetManyResult<RecordType>> => {
    // 空のデータを返す
    return {
      data: [] as RecordType[],
    };
  },
```

### APIからのレスポンスに値がない

- React-AdminはAPIからのレスポンスに値が存在することを求めてきます。
- なので、登録実行時なども何かしらの値を返却する必要があります。
- その場合、登録後の変更された値などを返すのが無難だと思います。

### 独自のメソッド

- React-Adminが用意したメソッド以外のメソッドを使用したい場合もあるかと思います。
- その際はindex.tsでこんな感じにすれば追加可能です。

```jsx
const customDataProvider = {
  ...dataProvider, 
  // 追加したいメソッド
  }
  
  export default customDataProvider;
```