React（Next.js/GraphQL）からHasuraを使ったCRUD操作のためのユーザーの一覧を表示させる部分の作成

## はじめに

CRUD操作のための処理部分をこれまで作成してきました。

詳細👇

https://qiita.com/ryosuketter/items/d9106135dd81bee3c038

前回は、Stateの定義やformコンポーネントの作成をしました。

https://qiita.com/ryosuketter/items/ce645c491f6df4e0688b

今回からは、ブラウザを通じでCRUD操作を行うためのインターフェイス部分を何回かに分けて作成していきます。

具体的には下記を作成していきます。

- Stateの定義
- クリックイベントの処理
- コンポーネント（レイアウト）の用意
    - form部分
    - ユーザーの一覧を表示させる部分 👈 本記事はコレについて書いてます

## 最終的なゴール

以下のような構成のアプリを作ることです。

![スクリーンショット 2021-10-18 17.27.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/e8a9a2e1-2723-4fec-77ef-be3b9eefdd81.png)

### 目的

- 仕事で使っている技術のキャッチアップと復習
- 使う可能性がある技術の理解度向上

## ユーザーの一覧を表示させる部分の作成

```tsx:pages/hasura-crud.tsx
const { data, error } = useQuery<GetUsersQuery>(GET_USERS, {fetchPolicy: 'cache-and-network'})
```

上記の処理で取得したユーザーの一覧情報を表示する部分のコンポーネントを作成します。

作成する場所は

https://qiita.com/ryosuketter/items/ce645c491f6df4e0688b

で、作成したformコンポーネントの下の部分にします。

ユーザー情報を表示する部分は別コンポーネントを作成します。

```
components/UserItem.tsx
```

コンポーネントの全体像は、下記のような感じです。

![Hasura_CRUD.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/0eb062fe-d4b7-517a-700e-07acc9bcdaeb.png)

1ユーザー単位で、作成ボタンや削除ボタンを作るイメージですね。

まずは、必要なモジュールをimportします。

```tsx:components/UserItem.tsx
import { VFC, memo, Dispatch, SetStateAction } from 'react'
import { Users, DeleteUserMutationFn } from '../types/generated/graphql'
```

表示最適化用に`memo`をインポート。

`Dispatch`と`SetStateAction`は、`useState`で作成された更新用のデータ型を定義する際に用います。

そしてGraphQLから自動生成されたデータ型から、Users と DeleteUserMutationFn をimportしています。

`DeleteUserMutationFn`こちらは、削除用の関数のデータ型です。

### コンポーネントのpropsの型情報

このコンポーネントに渡すpropsは、以下かなーと察しがつきますね。

- 表示するユーザーデータ
- 更新の関数（`setEditedUser`）
- 削除用の関数（`delete_users_by_pk`）

Propsのデータ型をジェネリクスで指定したいので、そのためのインターフェイスを作成します。

```tsx:components/UserItem.tsx
interface Props {
  user: {
    __typename?: 'users'
  } & Pick<Users, 'id' | 'name' | 'created_at'>
  delete_users_by_pk: DeleteUserMutationFn
  setEditedUser: Dispatch<
    SetStateAction<{
      id: string
      name: string
    }>
  >
}
```

`user`のデータ型は、`__typename`の属性と`Users`から必要なデータ（`id`, `name`, `created_at`）をPickしています。

`Users`の全データ型はホバーすると見れたりします。

![スクリーンショット 2021-11-19 16.57.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/8a194214-4f44-f898-f439-3466b3c59a90.png)

削除用の関数 `delete_users_by_pk` に必要な型は、import したモジュールをそのまま使用しています。

更新用の関数 `setEditedUser` に必要な型は、ホバーして表示させて、その内容をそのままコピペします。

### コンポーネントの作成

最終的に作るコンポーネントの構成は以下です。

![スクリーンショット 2021-11-19 17.49.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/97cd7646-8e6a-323c-29ff-e8652746633f.png)

表示部分や、更新用のボタンは難しくないと思いますので説明はSKIPします。

削除用ボタンは少し説明が必要かもしれません。

削除用ボタンのonClickの関数は通信処理が必要なので非同期で実行させています。

なので、onClickの中に無名アロー関数を入れて、クリック時にpropsで受け取った関数`delete_users_by_pk`が走るようにします。

関数`delete_users_by_pk`の実行には、削除対象のユーザーidが必要なので、それもuserとしてpropsで受け取っているので`user.id`指定しています。

```tsx
delete_users_by_pk({ variables: { id: user.id} })
```

最終的に完成したのが以下です。

```tsx:components/UserItem.tsx
import { VFC, memo, Dispatch, SetStateAction } from 'react'
import { Users, DeleteUserMutationFn } from '../types/generated/graphql'

interface Props {
  user: {
    __typename?: 'users'
  } & Pick<Users, 'id' | 'name'>
  delete_users_by_pk: DeleteUserMutationFn
  setEditedUser: Dispatch<
    SetStateAction<{
      id: string
      name: string
    }>
  >
}

export const UserItem: VFC<Props> = ({
  user,
  delete_users_by_pk,
  setEditedUser,
}) => {
  return (
    <div className="my-1">
      <span className="mr-2">{user.name}</span>
      <button
        className="mr-1 py-1 px-3 text-white bg-green-600 hover:bg-green-700 rounded-2xl focus:outline-none"
        data-testid={`edit-${user.id}`}
        onClick={() => {
          setEditedUser(user)
        }}
      >
        Edit
      </button>
      <button
        className="py-1 px-3 text-white bg-pink-600 hover:bg-pink-700 rounded-2xl focus:outline-none"
        data-testid={`delete-${user.id}`}
        onClick={async () => {
          await delete_users_by_pk({
            variables: {
              id: user.id,
            },
          })
        }}
      >
        Delete
      </button>
    </div>
  )
}
```

#### 再レンダリングを防止するための処理

上記のままだと、親コンポーネントにあるinput要素が更新するたびに、関数`setEditedUser`が走り、その都度

子コンポーネントである`<UserItem />`がレンダリングされます。

入力中の内容は、子コンポーネント`<UserItem />`に関係ないので、

親コンポーネントにあるinput要素が更新されても、子コンポーネント`<UserItem />`が再レンダリングされないようにします。

具体的には、子コンポーネント`<UserItem />`に`memo`を追加します。

```tsx
export const UserItem: VFC<Props> = memo(
  // 省略
)
```

最終的な完成形

```tsx:components/UserItem.tsx
import { VFC, memo, Dispatch, SetStateAction } from 'react'
import { Users, DeleteUserMutationFn } from '../types/generated/graphql'

interface Props {
  user: {
    __typename?: 'users'
  } & Pick<Users, 'id' | 'name'>
  delete_users_by_pk: DeleteUserMutationFn
  setEditedUser: Dispatch<
    SetStateAction<{
      id: string
      name: string
    }>
  >
}

export const UserItem: VFC<Props> = memo(
  ({ user, delete_users_by_pk, setEditedUser }) => {
    return (
      <div className="my-1">
        <span className="mr-2">{user.name}</span>
        <button
          className="mr-1 py-1 px-3 text-white bg-green-600 hover:bg-green-700 rounded-2xl focus:outline-none"
          data-testid={`edit-${user.id}`}
          onClick={() => {
            setEditedUser(user)
          }}
        >
          Edit
        </button>
        <button
          className="py-1 px-3 text-white bg-pink-600 hover:bg-pink-700 rounded-2xl focus:outline-none"
          data-testid={`delete-${user.id}`}
          onClick={async () => {
            await delete_users_by_pk({
              variables: {
                id: user.id,
              },
            })
          }}
        >
          Delete
        </button>
      </div>
    )
  }
)
```


### UserItemをimportする

先ほど作成したコンポーネントをimportしていきましょう。

formコンポーネントの下に表示させるので、以下の場所に書きます。

```tsx:pages/hasura-crud.tsx
</form>

{data?.users.map((user) => {
  return (
    <UserItem
      key={user.id}
      user={user}
      setEditedUser={setEditedUser}
      delete_users_by_pk={delete_users_by_pk}
    />
  )
})}
```

`data`がない場合もあるので`?`がつきます。`data?.users`は配列なので、関数`map`で要素1つ1つを処理できます。

最終的にreturnするコンポーネントは先ほど作成した`<UserItem />`で、渡すporpsは

```
user={user}
setEditedUser={setEditedUser}
delete_users_by_pk={delete_users_by_pk}
```

で、問題ないですね。（`key={user.id}`）は、ユニークに識別できるものの指定がいるので書いてるだけです。

今日のところは以上です。

次回は、完成した機能を実際に想定通りの挙動かを検証してみます。

## アウトプット100本ノック実施中

https://qiita.com/ryosuketter/items/713474dbd009870e4103