はじめに
CRUD操作のための処理部分をこれまで作成してきました。
詳細👇
前回は、Stateの定義やformコンポーネントの作成をしました。
今回からは、ブラウザを通じでCRUD操作を行うためのインターフェイス部分を何回かに分けて作成していきます。
具体的には下記を作成していきます。
- Stateの定義
- クリックイベントの処理
- コンポーネント(レイアウト)の用意
- form部分
- ユーザーの一覧を表示させる部分 👈 本記事はコレについて書いてます
最終的なゴール
以下のような構成のアプリを作ることです。
目的
- 仕事で使っている技術のキャッチアップと復習
- 使う可能性がある技術の理解度向上
ユーザーの一覧を表示させる部分の作成
const { data, error } = useQuery<GetUsersQuery>(GET_USERS, {fetchPolicy: 'cache-and-network'})
上記の処理で取得したユーザーの一覧情報を表示する部分のコンポーネントを作成します。
作成する場所は
で、作成したformコンポーネントの下の部分にします。
ユーザー情報を表示する部分は別コンポーネントを作成します。
components/UserItem.tsx
コンポーネントの全体像は、下記のような感じです。
1ユーザー単位で、作成ボタンや削除ボタンを作るイメージですね。
まずは、必要なモジュールをimportします。
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のデータ型をジェネリクスで指定したいので、そのためのインターフェイスを作成します。
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
の全データ型はホバーすると見れたりします。
削除用の関数 delete_users_by_pk
に必要な型は、import したモジュールをそのまま使用しています。
更新用の関数 setEditedUser
に必要な型は、ホバーして表示させて、その内容をそのままコピペします。
コンポーネントの作成
最終的に作るコンポーネントの構成は以下です。
表示部分や、更新用のボタンは難しくないと思いますので説明はSKIPします。
削除用ボタンは少し説明が必要かもしれません。
削除用ボタンのonClickの関数は通信処理が必要なので非同期で実行させています。
なので、onClickの中に無名アロー関数を入れて、クリック時にpropsで受け取った関数delete_users_by_pk
が走るようにします。
関数delete_users_by_pk
の実行には、削除対象のユーザーidが必要なので、それもuserとしてpropsで受け取っているのでuser.id
指定しています。
delete_users_by_pk({ variables: { id: user.id} })
最終的に完成したのが以下です。
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
を追加します。
export const UserItem: VFC<Props> = memo(
// 省略
)
最終的な完成形
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コンポーネントの下に表示させるので、以下の場所に書きます。
</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本ノック実施中