エンジニア1年目でApolloを使って良かったこと
こんにちは、元FEチームの城戸です。(現在は他チームに所属していますが、去年新卒入社して8月から今年の4月までFEチームとして活動してました!)
5月頭に食べログの飲食店向け管理画面に食べログノートという新しい予約管理機能がリリースされました🎉🎉🎉
食べログノートではUIが複雑な一部の画面をReactとNext.jsで実装してます。
詳細な技術スタックや食べログノートの概要に関しては上記の記事に書いているので、興味がある方はぜひ!
この記事ではApollo Clientを採用してよかった点を共有できればと思います。よろしくお願いします!
GraphQLとは
GraphQLはAPIのためのクエリ言語であり、URLに各リソースが紐付いているRESTful APIと比べると柔軟なデータフェッチが可能です。
//query
{
booking(id: “12b141a”){
name
menus{
name
}
}
}
//response
{
booking:{
name: “田中太郎”
menus:[
{
name: "焼肉定食"
},
{
name: "サバ味噌定食"
},
]
}
}
ApolloClientとは
ApolloClientはGraphQL APIのクライアントライブラリです。APIレスポンスを正規化やキャッシュしながらのデータ取得が可能であり、グローバルな状態管理もApolloで完結できます。
Apollo Clientを採用して感じたメリット
実装の中で感じた主なメリットは以下になります。
useQueryが優秀
const GET_BOOKINGS = gql`
query BookingList(
$visitDate: Date!
) {
bookings(
visitDate: $visitDate
) {
edges {
node {
id
date
timeFrom
timeTo
numberOfPeople
menus {
menu {
name
}
}
lastNameKana
firstNameKana
telephoneNumber
}
}
}
}
`
const BookingList: React.FC = () => {
const { loading, error, data } = useQuery(GET_BOOKINGS, {
variables: {
visitDate: date
},
});
if (loading) return '読み込み中';
if (error) return `Error! ${error.message}`;
return (
<>
{data.bookings.map(booking => (
<div key={booking.id} >
{booking.name}
</div>
))}
</>
)
}
上記では予約情報を取得するクエリ文字列をgql関数でラップし、GraphQLのクエリを作成しています。そして、作成したクエリをuseQueryという専用のHooksの引数に渡すことでデータをフェッチできます。
useQueryが返すオブジェクトには以下の値が含まれていて、各値がリアクティブに更新されることでUIも変化します。
loading:ロード状況
error:エラー内容
data:レスポンスの中身
データやローディング、エラーの状態に応じたUIの出し分けをシンプルに記述できるので、可読性高く実装することができました。
また、このHooksは他に細かいオプションも設定可能です。
以下は今回実装したタイムスケジュール画面です。
現在の予約状況を正確に把握するため、一定間隔で再フェッチをする処理が必要でしたが、下記のようにHooksにポーリングのオプションを設定するだけで済んだので、作業時間を短縮できました🎉
const POLL_INTERVAL = 60000;
const { loading, error, data } = useQuery(GET_BOOKINGS, {
variables: {
visitDate: date
},
pollInterval: POLL_INTERVAL,
});
型 & Custom Hooksの自動生成が強力
GraphQL Code Generatorを使うことでGraphQLのスキーマからTypeScriptの型の自動生成を行うことが可能です。
まずはGraphQL Code Generatorの設定ファイルであるcodegen.ymlを下記のように作成します。
schema: ./schema.graphql
documents:
- ./src/queries/*.gql
generates:
./src/types.ts:
plugins:
- typescript
config:
enumsAsTypes: true
./src/queries:
preset: near-operation-file
presetConfig:
baseTypesPath: ../types.ts
plugins:
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
enumsAsTypes: true
hooks:
afterOneFileWrite:
- node scripts/generateEntrypoint && prettier --write
ファイル内の主な設定項目については以下になります。
schema:スキーマが取得できるGraphQLエンドポイントのURLやローカルの.graphqlファイルを指定します。
documents:クエリ文字列を記述した.gqlファイルのパスを指定します。
generates:生成するファイルパスやプラグインといった出力周りの設定を行います。
下記のように予約情報をフェッチするクエリを.gqlファイルに記述し、先ほどのdocumentsで指定した場所に置きます。
query BookingList (
$visitDate: Date!
) {
bookings {
id
date
timeFrom
timeTo
numberOfPeople
lastNameKana
telephoneNumber
menus {
menu {
name
}
}
}
}
生成コマンドを実行すると、下記のようにBookingListのクエリに関する型が記載されたファイルが生成されます。
export type BookingListQueryVariables = Types.Exact<{
visitDate: Types.Scalars["Date"];
}>;
export type BookingListQuery = {
__typename?: "Query";
bookings: {
__typename?: "BookingConnection";
edges?: Types.Maybe<
Array<
Types.Maybe<{
__typename?: "BookingEdge";
node?: Types.Maybe<{
__typename?: "Booking";
id: string;
date: any;
timeFrom: any;
timeTo: any;
numberOfPeople: number;
lastNameKana?: Types.Maybe<string>;
firstNameKana?: Types.Maybe<string>;
telephoneNumber?: Types.Maybe<string>;
menus: Array<{
__typename?: "BookingMenu";
menu: { __typename?: "Menu"; name: string };
}>;
}>;
}>
>
>;
};
};
export const BookingListDocument = gql`
query BookingList(
$visitDate: Date!
) {
bookings(
visitDate: $visitDate
) {
edges {
node {
id
date
timeFrom
timeTo
numberOfPeople
menus {
menu {
name
}
}
lastNameKana
firstNameKana
telephoneNumber
}
}
}
}
`;
さらに@graphql-codegen/typescript-react-apolloを導入すると、下記のようにuseQueryやuseMutationをラップした専用のCustom Hooksの自動生成まで行ってくれます。
export function useBookingListQuery(
baseOptions: Apollo.QueryHookOptions<
BookingListQuery,
BookingListQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<BookingListQuery, BookingListQueryVariables>(
BookingListDocument,
options
);
}
自動生成した型とhooksを使用した最終形は以下になります。
const BookingList: React.FC = () => {
const { loading, error, data } = useBookingListQuery({
variables: {
visitDate: date,
},
});
if (loading) return '読み込み中';
if (error) return `Error! ${error.message}`;
return (
<>
{data.bookings.map(booking => (
<div key={booking.id} >
{booking.name}
</div>
))}
</>
)
}
自動生成前と比べて記述量を削減することができ、Hooksから取得できる値に型が付いていることでエディタの補完も効きます。
食べログノートは新規のプロジェクトであり、1からの開発だったため、実装の過程でスキーマ修正が頻発していました。本来はスキーマ修正が発生するたびに、関連する型、クエリ、実装箇所など諸々を手で直していく必要がありますが、型&Custom Hooksの自動化によって、修正箇所を最小限に留めつつ、開発が進められたと思います🎉
グローバルな状態管理も可能
Apollo ClientにはReactive Variablesというグローバルな状態管理を可能にする機能もあります。
食べログノートのタイムスケジュール画面では予約カセットをドラッグ&ドロップで移動させることで、予約席と予約時間を変更することができます。
予約カセットのドラッグを開始すると編集モードになり、下から編集バーが出てきたり、他の予約カセットがドラッグできなくなったり、ポーリングが止まったり、様々な処理が走ります。そのため、ドラッグ中の予約カセットの状態を複数のコンポーネントで取得する必要があり、Reactive Variablesを活用しました。
まず下記のようにtemporaryBookingVarというReactive Variablesを定義して、編集モード、ドラッグ、予約情報の状態を持たせます。
const temporaryBookingVar = makeVar<TemporaryBooking>({
isEditingMode: false,
bookingId: null,
timeFrom: null,
timeTo: null,
scheduleTimeTo: null,
seatIds: null,
isDragging: false,
unassignedBookingsSeatRowIndex: null,
});
次に下記のように値の更新処理を行う関数と値自身を返すCustom Hooksを作成することで、Hooksを介して状態管理を行うように実装をしています。値を返す際はuseReactiveVarを挟むことで、コンポーネントの再描画も行います。
export function useTemporaryBookingData(): {
temporaryBookingData: TemporaryBooking;
setEditingMode: (bookingId: string) => void;
resetEditingMode: () => void;
setTemporaryBookingTimeAndSeats: (
timeFrom: Dayjs,
timeTo: Dayjs,
scheduleTimeTo: Dayjs,
seatIds: Array<string> | null,
) => void;
setIsDragging: (isDragging: boolean) => void;
} {
const setEditingMode = (bookingId: string) => {
temporaryBookingVar({
isEditingMode: true,
bookingId: bookingId,
timeFrom: null,
timeTo: null,
scheduleTimeTo: null,
seatIds: null,
isDragging: false,
});
};
const resetEditingMode = () => {
temporaryBookingVar({
isEditingMode: false,
bookingId: null,
timeFrom: null,
timeTo: null,
scheduleTimeTo: null,
seatIds: null,
isDragging: false,
});
};
const setTemporaryBookingTimeAndSeats = (
timeFrom: Dayjs,
timeTo: Dayjs,
scheduleTimeTo: Dayjs,
seatIds: Array<string> | null,
) => {
temporaryBookingVar({
...temporaryBookingVar(),
timeFrom,
timeTo,
scheduleTimeTo,
seatIds,
});
};
const setIsDragging = (isDragging: boolean) => {
temporaryBookingVar({
...temporaryBookingVar(),
isDragging,
});
};
return {
temporaryBookingData: useReactiveVar(temporaryBookingVar),
setEditingMode,
resetEditingMode,
setTemporaryBookingTimeAndSeats,
setIsDragging,
};
}
最後に各コンポーネントで上記のCustom Hooksを呼び出すことで、状態の取得・更新を行います。
const {
temporaryBookingData: { isDragging },
resetEditingMode,
} = useTemporaryBookingData();
グローバルな状態管理までApolloで完結できたことで、ReduxやRecoilなどの状態管理ライブラリを別途追加せずに済みました。実装スケジュール的にはあまり余裕がなかったので、追加ライブラリのキャッチアップに必要な時間を実装に当てることができたのも大きかった気がします🎉
まとめ
今回は食べログノートの実装をする中で感じたApollo Clientのメリットを紹介しました。Apollo ClientをGraphQLのクライアントとして使用し、データやローディング状態によるUIの出し分けをシンプルに実装できました。また、GraphQL Code Generatorによる型やHooksの自動生成を行うことで、開発中の仕様変更にも柔軟に対応できました。さらにReactive Variablesを使うことで、ライブラリの依存関係を最小限にすることができました。
フェッチや状態管理のコードがシンプルだったことは、エンジニア歴のまだ浅い私としてはキャッチアップ面でも実装面でも非常にありがたかったです。また、Apolloはドキュメントや周辺ツールが非常に充実しており、チームの輪読会でドキュメントのインプットをしつつ、chrome extensionなども上手く活用しながら、実装を進めることができたことも良かったと思います。
最後に
現在、食べログではフロントエンドに関わるポジションとして以下の2つを募集しています。
気になったかたは是非チェックしてみてください!
・フロントエンド統括チームに所属するフロントエンドエンジニア
・フロントエンドをメインにサービス開発を担当していくWEBエンジニア
どれかに当てはまった方は以下のリンクも是非御覧ください!