Slide 1

Slide 1 text

ApolloClientとGraphQL Code Generatorの話 淺津 @asazutaiga

Slide 2

Slide 2 text

ブラウザ上でのJavaScriptの関心(現代版)

Slide 3

Slide 3 text

見た目 マークアップ(HTML) スタイリング(CSS) → React, MUI 処理 API通信 状態管理 →Apollo Client

Slide 4

Slide 4 text

Apollo Client

Slide 5

Slide 5 text

GraphQLクライアント 公式ドキュメントはReactベースだが、他ライブラリでも使える

Slide 6

Slide 6 text

API

Slide 7

Slide 7 text

ApolloClient , ApolloProvider , Link ApolloClient インスタンスを の client 引数にセット Link を挟むことでデータフローをカスタマイズ InMemoryCache が自動的に取得データをメモリ上にキャッシュしてくれる 手動更新も可能

Slide 8

Slide 8 text

const authLink = setContext((_, { headers }) => { const token = localStorage.getItem('token') debugger return { headers: { ...headers, authorization: `Bearer ${token}` ?? '', }, } }) const httpLink = new HttpLink({ uri: config.API_URL + '/graphql', }) const client = new ApolloClient({ link: authLink.concat(httpLink), uri: 'http://loclahost:8080/graphql', cache: new InMemoryCache(), }) export const AppApolloProvider: React.VFC<{ children: ReactNode }> = ({ children, }) => { return {children} }

Slide 9

Slide 9 text

useQuery gql で作成したquery objectをもとにデータを取得する サーバーのスキーマ定義に則ってください loading , error , data を返す 非同期処理を完全にラップするのでView層に Promise が登場しない コンポーネントがマウントされたタイミングでqueryが実行される キャッシュがある場合はキャッシュのデータを利用する

Slide 10

Slide 10 text

import React from 'react' import { useQuery, gql } from '@apollo/client' interface RocketInventory { id: number model: string year: number stock: number } interface RocketInventoryData { rocketInventory: RocketInventory[] } interface RocketInventoryVars { year: number } const GET_ROCKET_INVENTORY = gql` query GetRocketInventory($year: Int!) { rocketInventory(year: $year) { id model year stock } } `

Slide 11

Slide 11 text

export const RocketInventoryList: React.VFC = () => { const { loading, error, data } = useQuery( GET_ROCKET_INVENTORY, { variables: { year: 2019 } }, ) if (error) return <>{error.message}> return (

Available Inventory

{loading ? (

Loading ...

) : ( Model Stock {data && data.rocketInventory.map((inventory) => ( {inventory.model} {inventory.stock} ))} )}
) }

Slide 12

Slide 12 text

useMutation 基本は useQuery と同様 タプルで一つ目の戻り値に mutation のトリガーが、二つ目の戻り値に useQuery と同 じような loading , error , data が返る

Slide 13

Slide 13 text

import React, { useState } from 'react' import { useMutation, gql } from '@apollo/client' const SAVE_ROCKET = gql` mutation saveRocket($rocket: RocketInput!) { saveRocket(rocket: $rocket) { model } } ` interface RocketInventory { id: number model: string year: number stock: number } interface NewRocketDetails { model: string year: number stock: number }

Slide 14

Slide 14 text

export const NewRocketForm: React.VFC = () => { const [model, setModel] = useState('') const [year, setYear] = useState(0) const [stock, setStock] = useState(0) const [saveRocket, { loading, error, data }] = useMutation<{ saveRocket: RocketInventory }, { rocket: NewRocketDetails }>( SAVE_ROCKET, { variables: { rocket: { model, year: +year, stock: +stock } }, } ) return (

Add a Rocket

{error ?

Oh no! {error.message}

: null} {data && data.saveRocket ?

Saved!

: null}

Model setModel(e.target.value)} />

Year setYear(+e.target.value)} />

Stock setStock(e.target.value)} />

model && year && stock && saveRocket()}>Add
) }

Slide 15

Slide 15 text

readQuery , writeQuery etc... キャッシュを直接読み書きするためのAPI 基本的には不整合のもとになるので writeQuery とかしないほうがよいのでは? サーバー側が処理結果をいい感じに返してないとやる必要が出ることもありそう? (要検証) 基本的には useQuery がキャッシュ管理をラップしてくれてるので readQuery するこ とは少ないのでは?

Slide 16

Slide 16 text

local only state ( @client ) サーバー側に存在しない状態=ローカルにのみ存在する状態を管理する なんかフラグ程度にしか使え無さそうな印象はある query ProductDetails($productId: ID!) { product(id: $productId) { name price isInCart @client # This is a local-only field } }

Slide 17

Slide 17 text

local only stateの定義 Cacheのフィールドポリシーで定義することで使えるようになる 例は localStorage に保持しているが他の場所にもおけそうですね const cache = new InMemoryCache({ typePolicies: { // Type policy map Product: { fields: { // Field policy map for the Product type isInCart: { // Field policy for the isInCart field read(_, { variables }) { // The read function for the isInCart field return localStorage.getItem('CART').includes( variables.productId ) } } } } } })

Slide 18

Slide 18 text

ちなみに フィールドポリシーはfetched stateにも使えます selector とか computed に近い使い方も工夫次第でできそう? と思ったけど読み出しタイミングで他のフィールドへの参照はできないので、その他の 状態管理ライブラリよりは弱い const cache = new InMemoryCache({ typePolicies: { Person: { fields: { name: { read(name) { // Return the cached name, transformed to upper case return name.toUpperCase() } } }, }, }, })

Slide 19

Slide 19 text

Apollo Clientの便利そうなところ hooksが通信状態をラップしてくれる InMemoryCache がキャッシュやデータの加工をしてくれる Apollo Clientのイマイチなところ データモデル(≒ サーバーのスキーマ定義)とかけ離れた状態を持つのは厳しそう それは設計が悪いのでは?みたいな話でもある

Slide 20

Slide 20 text

GraphQL Code Generator

Slide 21

Slide 21 text

.graphql からJavaScript(TypeScript)のコードを生成してくれる 公式プラグインでApollo Clientに対応

Slide 22

Slide 22 text

codegen.yml schema: ./schema.graphql # or http://localhost:8080/graphql documents: ./graphql/**/*.graphql generates: ./src/generated/graphql.tsx: plugins: - typescript - typescript-operations - typescript-react-apollo config: withHOC: false withComponent: false withHooks: true namingConvention: typeNames: change-case#pascalCase enumValues: change-case#pascalCase transformUnderscore: true hooks: afterOneFileWrite: - prettier --write

Slide 23

Slide 23 text

// schema.graphql enum Linked { ACCOUNT_NOT_FOUND APPROVAL APPROVAL_REQUEST DENIAL EXPULSION PRIVATE_ACCOUNT SELF_DENIAL }

Slide 24

Slide 24 text

npx graphql-codegen

Slide 25

Slide 25 text

export enum Linked { AccountNotFound = 'ACCOUNT_NOT_FOUND', Approval = 'APPROVAL', ApprovalRequest = 'APPROVAL_REQUEST', Denial = 'DENIAL', Expulsion = 'EXPULSION', PrivateAccount = 'PRIVATE_ACCOUNT', SelfDenial = 'SELF_DENIAL', }

Slide 26

Slide 26 text

query SnsConnects($first: Int = 10, $linked: Linked = APPROVAL_REQUEST) { snsConnects(first: $first, linked: $linked) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount edges { cursor node { id name followerCount createdAt userInfluencers { id name image snsConnects { id linked provider } } } } } }

Slide 27

Slide 27 text

npx graphql-codegen

Slide 28

Slide 28 text

export type SnsConnectsQuery = { __typename?: 'Query' snsConnects?: | { __typename?: 'SnsConnectConnection' totalCount?: number | null | undefined pageInfo: { __typename?: 'PageInfo' hasNextPage: boolean hasPreviousPage: boolean startCursor?: string | null | undefined endCursor?: string | null | undefined } edges?: | Array< | { __typename?: 'SnsConnectEdge' cursor: string node?: | { __typename?: 'SnsConnect' id: string name: string followerCount: number createdAt: any userInfluencers: { __typename?: 'UserInfluencer' id: string name?: string | null | undefined image?: string | null | undefined snsConnects: Array<{ __typename?: 'SnsConnect' id: string linked: Linked provider: Provider }> } } | null | undefined } | null | undefined > | null | undefined } | null | undefined }

Slide 29

Slide 29 text

pluginで useQuery などのラッパーも生成してくれる export function useSnsConnectsQuery( baseOptions?: Apollo.QueryHookOptions< SnsConnectsQuery, SnsConnectsQueryVariables >, ) { const options = { ...defaultOptions, ...baseOptions } return Apollo.useQuery( SnsConnectsDocument, options, ) }

Slide 30

Slide 30 text

GraphQL Code Generatorまとめ schemaという共通言語を通して「正しい型」がGraphQL server ⇆ Frontendで共有 自分で gql や useQuery を書くことはなくなるかも schemaを取得する必要があるので、monorepoなどの選択肢が出てくる? or schema自体にバージョン埋め込みなどの話も必要になるのかも