Open8

react-relay チュートリアル

ikeponikepon
  • Relay compiler
    • GraphQL を処理し、Relay でinputやクエリの結果を表すのに必要な TS ファイルなどの追加ファイルを作成する
    • 変更があれば自動で追従する
    • relay-compiler --watch ってのが実行コマンドかな
    • GraphQL に間違いがあればログにエラーが出力される
  • あとで調べる
    • App.tsx の RelayEnvironment
  • self-contained という言葉がチラホラ出ている
    • コンポーネントで自己完結するという思想らしい
    • 必要なデータは親から渡ってくるとかじゃなくて、コンポーネント自体で必要なものを要求するってイメージ
    • そのために、クエリをフラグメントに分割する
ikeponikepon

GraphQL and Relay

GraphQL

  • Node: データ
  • Edge: データ間の繋がり
  • schema.graphql
    • Query のところに root node がある
      • ここを軸に query を作るんだと思う

Relay

  • コンポーネントで必要なデータを定義すると Relay がいい感じにまとめてリクエストしてくれる

  • 取得したデータは Store と呼ばれる local cache で管理され、各コンポーネントが必要としているデータをいい感じで提供してくれる

  • Store でデータが更新されれば、自動で対応するコンポーネントにも配ってくれるっぽい
ikeponikepon

Query Basics

  • query は graphql`` で書かれた string literal
  • これによって Relay Compiler は JS 内の GraphQL を見つけ、コンパイルする
  • query は root node の fields があり、edge を置くこともある
    • edge をおいた場合は {} 内に edge の先の node の field を書く
    • 以下のコードと画像を照らし合わせて node と edge の関係を見るとわかりやすい
  • コンパイルしたファイルは src/components/__generated__ 以下に置かれる
import { graphql } from 'relay-runtime';

const NewsfeedQuery = graphql`
  query NewsfeedQuery {
    topStory {
      title
      summary
      poster {
        name
        profilePicture {
          url
        }
      }
      thumbnail {
        url
      }
    }
  }
`;

  • query を定義したらやること 2 つ
    • npm run relay を走らせる
      • package.json の記載: "relay": "relay-compiler"
      • 作成した GraphQL query を知らせる
    • fetch したデータを使うように React component を修正する
      • useLazyLoadQuery
        • GraphQL query とサーバーに query と一緒に渡す変数を引数に取る
        • コンポーネントが最初に render されるときに data を fetch する
        • ここで取得したデータの型はコンパイルした __generated__ 以下にあるので参照できる
        • 具体的に言うと Relay のページの以下の部分
import type {NewsfeedQuery as NewsfeedQueryType} from './__generated__/NewsfeedQuery.graphql';

function Newsfeed({}) {
  const data = useLazyLoadQuery
  <NewsfeedQueryType>
  (NewsfeedQuery, {});
  ...
}

★ ここでやってるのは、元の schema.graphql はあるとして、それも含めてコンポーネントで必要なものを定義して、それを schema から取得してるんだと思う

試しに name って無関係なものを定義して compile してみる

const NewsFeedQuery = graphql`
  query NewsfeedQuery {
    topStory {
      title
      summary
+      name
      poster {
        name
        profilePicture {
          url
        }
      }
      thumbnail {
        url
      }
    }
  }
`;

んで、 compile するとエラーになるので、たぶん理解は合ってると思う

➜  newsfeed git:(main) ✗ npm run relay

> newsfeed@0.0.0 relay
> relay-compiler

[INFO] [default] compiling...
[ERROR] Error in the project `default`: ✖︎ The type `Story` has no field `name`.

  src/components/Newsfeed.tsx:12:7
   11 │       summary
   12 │       name
      │       ^^^^
   13 │       poster {

[ERROR] Compilation failed.
[ERROR] Unable to run relay compiler. Error details:
Failed to build:
 - Validation errors:
 - The type `Story` has no field `name`.:src/components/Newsfeed.tsx:72:76

★ この章でやったことの整理

  • graphql`` でコンポーネントで必要なデータのクエリを定義
  • npm run relay( relay-compiler ) で __generated__ 配下に上記で定義したクエリの型を作成
  • 上記で作成したクエリを使ってデータ取得
    • const data = useLazyLoadQuery<NewsfeedQueryType>(NewsfeedQuery, {});
      
    - useLazyLoadQuery: relay で用意されており、これを使ってコンポーネントレンダリング時にリクエストが投げられる
    - NewsfeedQuery: 作成したクエリ
    - NewsfeedQueryType: npm run relay で作成された型
ikeponikepon

Fragments

フラグメントを利用することで、1つのクエリでのリクエストを維持したまま、各コンポーネントが必要とするデータを個別に宣言することができる

★ 以下の画像の話

  • ここまでの実装の話
    • Newsfeed.tsx が子の Story.tsx で必要なデータの管理してるのはなんで?
    • 子の Story.tsx から孫に渡してる場合は?また、データを追加する場合も関係ない親のコンポーネントをいじるの?
    • これを各コンポーネントで必要なデータを定義していく形に変える、そのために使うのが Fragment

★ Fragment の手順が tutorial 内で書かれているが、5割理解ってかんじなので、あとで読み直す

  • Story.tsx で必要なデータのフラグメントを Story.tsx 内で定義する
    const StoryFragment = graphql fragment StoryFragment on Story { title summary createdAt poster { name profilePicture { url } } thumbnail { url } };
    • Newsfeed.tsx の NewsfeedQuery を上記のフラグメントで置き換える
      • NewsfeedQuery は topStory で root のクエリを定義しているので必要
      • その中身は Story.tsx が使うので、そこで Fragment を使って定義しているんだと思う
    • これによって self-contained (自己完結型) ってのを実現している
    • Fragment の定義は必要な Story.tsx にあるので、 Story に必要なものがあればそこに定義して Newsfeed.tsx には変更が入らないようになる

手順は Exercise に書いてある通りな感じ

The PosterByline component used by Story renders the poster’s name and profile picture. Use these same steps to fragmentize PosterByline. You need to:

- Declare a PosterBylineFragment on Actor and specify the fields it needs (name, profilePicture). The Actor type represents a person or organization that can post a story.
- Spread that fragment within poster in StoryFragment.
- Call useFragment to retrieve the data.
- Update the Props to accept a PosterBylineFragment$key as the person prop.

const PosterBylineFragment で定義して、他のコンポーネントで使うときに export してないのが気になる

  • たぶん、 graph 内で同名の定義があるので、 graph 内であれば export せずに使えるんだと思う

  • Fragment arguments: フラグメント引数

    • フラグメントの field が受け取る引数を定義する
      • パラメータ名
      • type
      • defaultValue: optionally
        • 定義しない場合は required になる
const ImageFragment = graphql`
  fragment ImageFragment on Image
    @argumentDefinitions(
      width: {
        type: "Int",
        defaultValue: null
      }
      height: {
        type: "Int",
        defaultValue: null
      }
    )
  {
    url(
      width: $width,
      height: $height
    )
    altText
  }
`;
  • フラグメント引数は、GraphQL のサーバー側に伝える情報として使われる
    • 以下はサーバー側で受け取ったリクエストのパラメータ
    • GraphQL フラグメントの引数になってるっぽい
[server] request params:  {
[server]   query: 'query NewsfeedQuery {\n' +
[server]     '  topStory {\n' +
[server]     '    ...StoryFragment\n' +
[server]     '    id\n' +
[server]     '  }\n' +
[server]     '}\n' +
[server]     '\n' +
[server]     'fragment ImageFragment_2XXOlY on Image {\n' +
[server]     '  url(width: 100, height: 100)\n' +
[server]     '  altText\n' +
[server]     '}\n' +
[server]     '\n' +
[server]     'fragment ImageFragment_OxVt3 on Image {\n' +
[server]     '  url(width: 400)\n' +
[server]     '  altText\n' +
[server]     '}\n' +
[server]     '\n' +
[server]     'fragment PosterBylineFragment on Actor {\n' +
[server]     '  __isActor: __typename\n' +
[server]     '  name\n' +
[server]     '  profilePicture {\n' +
[server]     '    ...ImageFragment_2XXOlY\n' +
[server]     '  }\n' +
[server]     '}\n' +
[server]     '\n' +
[server]     'fragment StoryFragment on Story {\n' +
[server]     '  createdAt\n' +
[server]     '  title\n' +
[server]     '  summary\n' +
[server]     '  thumbnail {\n' +
[server]     '    ...ImageFragment_OxVt3\n' +
[server]     '  }\n' +
[server]     '  poster {\n' +
[server]     '    __typename\n' +
[server]     '    ...PosterBylineFragment\n' +
[server]     '    id\n' +
[server]     '  }\n' +
[server]     '}\n',
[server]   variables: {}
[server] }

GraphQL の定義(schema.graphql) を見ると height と width が定義されてる

type Image {
  url(height: Int, width: Int): String!
  altText: String
}
ikeponikepon

Arrays and Lists

スキーマは各フィールドがリストであるかどうかを指定しますが、奇妙なことに、GraphQLのクエリ構文は単一フィールドの選択とリストの選択を区別しません。

  • request
query MyQuery {
  viewer {
    contacts { // List of edges
      id // field on a single item
      name
    }
  }
}
  • response
{
  viewer: {
    contacts: [ // array in response
      {
        id: "123",
        name: "Chris",
      },
      {
        id: "789",
        name: "Sue",
      }
    ]
  }
}

★ Graphql の schema 上の区別はないが、以下にあるように単数形と複数形で GraphQL 側がいい感じに処理して単数と List を返してくれるんだと思う

As it happens, the schema in our example app has a topStories field that returns a list of Stories, as opposed to the topStory field we're currently using that returns just one.
ikeponikepon

Refetchable Fragments

  • Relay は component ごとに必要なクエリをまとめて実行する
  • ただ、コンポーネントごとに部分的に更新したい場合がある
    • 特定テーブルのみ検索して表示を切り替えるなど
  • そういった場合に @refetchable ディレクティブを使うことで部分的にクエルを実行できる
    • これも Fragment の一部
    • Fragment は元クエリがあって初めて実行される
    • Fragment だけでは動かないが、@refetchable ディレクティブを設定することで Relay がうまく処理して部分的に実行してくれる
    • 今までは useFragment を使っていたが、これを useRefetchableFragment に変更する
      • それによってパラメータも変わるので注意
  • useTransition
    • React の状態更新を制御する
    • fetch している間、既存データを表示し、データ取得後にページを更新する
      • この更新を待つのが useTransition
ikeponikepon
  • Relay における Mutation

    • useMutation を使う
    • サーバーに送る変数に ID が含まれている場合、Relay がいい感じにレスポンスから store を更新してくれるので、手動で store の値を更新しに行く必要はない
    • これはMutationのレスポンスを受けてからデータを更新する
    • そのため、更新までの待ち時間が存在する
      • Loading を表示することもある
  • Mutation のリクエストのレスポンスを待たないで更新したい

    • たとえば、Post にコメントを書いて、投稿ボタンを押す、レスポンスがあるまで待つんじゃなくて、画面上は反映、リクエストが失敗したときに戻すってイメージ
    • Relay はこの辺の仕組みも持っている
      • optimistic update: 楽観的更新
        • ★ 更新されるでしょうって前提で進めるってかんじだと思う
      1. 楽観的な更新で、更新したい情報をローカルデータストアに反映する
      2. その後、サーバーにリクエストを実行する
      3. サーバーからのレスポンスを待って
      • 成功の場合: サーバーからのレスポンスを使ってローカルデータストアを更新する
      • 失敗した場合: 最初の状態に戻る