react-adminのdataProviderのエラーとnotificationをカスタムする

# はじめに
[react-admin](https://marmelab.com/react-admin/)は規模のわりにドキュメントが薄くてソースコードやサンプルで読み解くしかないことがけっこうある気がする。[dataProvider](https://marmelab.com/react-admin/DataProviders.html)をカスタムする際にもドキュメントに書かれていることだけではぶっちゃけあんまり実用的なことができない。

今回はカスタムしたdataProviderで発生したエラーをNotification表示するのをちょっといじりたいなというあるあるなケースでいろいろ調べ回って時間を食ったので忘れないように書いておこうと思いました。

# dataProviderでのエラー処理
まずdataProviderはPromiseを返さなければならない。エラーが発生した場合Notificationのイベントをどう発生させるかについては[Error Format](https://marmelab.com/react-admin/DataProviders.html#error-format)に書いてある実装がベターな感じ。HttpErrorオブジェクトを生成してPromise.rejectを返す。

```customDataProvider.ts
const dataProvider = async (type: string, resource: string, params: any): Promise<any> => {
  try {
    /*
     * custom logic for data provider ...
     */
    const result = await nodeFetch(url, options);
    if (result.status < 200 || result.status >= 300) {
      const error = await result.json();
      throw error;
    }
    const res = await result.json();
    const response = {
      data: res,
    };
    return response;
  } catch (e) {
    throw new HttpError(e.message, e.statusCode, e);
  }
};
```

こんな感じでサーバーからエラーステータスで返ってきたメッセージをHttpErrorにしてthrowすればよい。HttpErrorはreact-adminがErrorオブジェクトを拡張しているもので仕様についてはドキュメントには見当たらなかったので[ソースコード](https://github.com/marmelab/react-admin/blob/43c4fafc8bcedebc386c7d3dc3b63cfd56420a17/packages/ra-core/src/dataProvider/HttpError.ts)を読むしかないかも。

# onFailure ハンドラ
上記でthrowした例外をReactコンポーネント側でどのようにフックするかについては[onFailure](https://marmelab.com/react-admin/CreateEdit.html#onfailure)に書かれている。リソースの`<Create>` `<Edit>` コンポーネントでは `onFailure` プロパティによってエラーイベントをフックしてNotificationをカスタマイズすることができる。

```Sample.tsx
export const SampleCreate = (props: any) => {
  const notify = useNotify();
  const onFailure = (error: any) => {
    notify(error.message, 'warning');
  };
  return (
    <Create {...props} onFailure={onFailure}>
      <SimpleForm>
        <TextInput source="name" validate={required()} />
        <TextInput source="description" validate={required()} multiline />
      </SimpleForm>
    </Create>
  );
};

export const SampleEdit = (props: any) => {
  const notify = useNotify();
  const refresh = useRefresh();
  const onFailure = (error: any) => {
    notify(error.message, 'warning');
    refresh();
  };
  return (
    <Edit {...props} onFailure={onFailure}>
      <SimpleForm>
        <TextInput source="name" validate={required()} />
        <TextInput source="description" validate={required()} multiline />
      </SimpleForm>
    </Edit>
  );
};

```

`useNotify`, `useRefresh`などについては[useDataProvider](https://marmelab.com/react-admin/Actions.html#handling-side-effects-in-usedataprovider)にさらっと書いてある。`notify()`の表示タイプを示す第2引数の種類に関してドキュメントのどこにも見当たらないが、[notificationActions.ts](https://github.com/marmelab/react-admin/blob/43c4fafc8bcedebc386c7d3dc3b63cfd56420a17/packages/ra-core/src/actions/notificationActions.ts#L3)によると`success` `info` `warning` `error`の4タイプがある。指定するタイプによってNotificationの背景色が変わるようだ。

`onFailure`と同じように処理成功時のイベントを`onSuccess`でカスタマイズすることが可能である。これらを`<Edit>`で扱うときに注意すべきなのが[mutationMode](https://marmelab.com/react-admin/CreateEdit.html#mutationmode)。Optimistic renderingが有効になる`optimistic`, `undoable`(default) の場合NotificationはdataProviderに処理が適用される前に副作用として発生する。よって処理の仕方によっては一見正常にデータが更新されたように見えたあと（undoableの場合はローカルでの正常処理の5秒後）にエラーNotificationが表示されることになる。これを嫌って `mutationMode="pessimistic"` としてOptimistic renderingを無効にするかどうかはUXによりケースバイケースになるだろう。

このあたりは色々試してみたが個人的な趣味としては、とりあえず上記のようにundoableでEditコンポーネントを表示したままエラーを出すのが良いと思った。一点、エラー表示のあと`refresh()`を実行してフォーム値を更新前の状態に戻すことで再編集を促すようにするのがよいかなと思う。`<Edit>`のデフォルト挙動は処理成功時に`<List>`にリダイレクトするので画面遷移の違いでもエラーに気づきやすいはず。

# authProviderのcheckError
dataProviderでthrowされた例外はauthProviderの[checkError](https://marmelab.com/react-admin/Authentication.html#request-format)でも補足される。なので認証関係のエラーなのかその他のエラーなのかを区別しておかないといけない。以下のようなテキトーな処理だとAPIエラーが発生するたびにログイン画面に戻ることになるので下記ではerrorの内容によってここで例外をthrowするかどうかを判断したほうがいいかもしれない。

```customAuthProvider.ts
  ...
  checkError: async (error: any): Promise<any> => {
    if (error) {
      throw error;
    }
    return;
  },
  ...
```

# おわりに
各コンポーネントの関係性が把握できればわりとゆるい仕様で柔軟にカスタマイズできることがわかるが、ここにたどり着くのに時間がかかった。もうちっとケースごとのベストプラクティスなコードがウェブに散在してても良い気がするんだけど意外とないよなreact-admin。便利だと思うんだけど。