react-relay チュートリアル

これをやっていく

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

GraphQL and Relay
GraphQL
- Node: データ
- Edge: データ間の繋がり
- schema.graphql
- Query のところに root node がある
- ここを軸に query を作るんだと思う
- Query のところに root node がある
Relay
- コンポーネントで必要なデータを定義すると Relay がいい感じにまとめてリクエストしてくれる
- 取得したデータは Store と呼ばれる local cache で管理され、各コンポーネントが必要としているデータをいい感じで提供してくれる
- Store でデータが更新されれば、自動で対応するコンポーネントにも配ってくれるっぽい

Query Basics
- query は graphql`` で書かれた string literal
- これによって Relay Compiler は JS 内の GraphQL を見つけ、コンパイルする
- query は root node の fields があり、edge を置くこともある
- edge をおいた場合は
{}
内に edge の先の node の field を書く - 以下のコードと画像を照らし合わせて node と edge の関係を見るとわかりやすい
- 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 を知らせる
- package.json の記載:
- 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 で作成された型
-

Fragments
フラグメントを利用することで、1つのクエリでのリクエストを維持したまま、各コンポーネントが必要とするデータを個別に宣言することができる
★ 以下の画像の話
- ここまでの実装の話
- Newsfeed.tsx が子の Story.tsx で必要なデータの管理してるのはなんで?
- 子の Story.tsx から孫に渡してる場合は?また、データを追加する場合も関係ない親のコンポーネントをいじるの?
- これを各コンポーネントで必要なデータを定義していく形に変える、そのために使うのが Fragment
★ Fragment の手順が tutorial 内で書かれているが、5割理解ってかんじなので、あとで読み直す
- Story.tsx で必要なデータのフラグメントを Story.tsx 内で定義する
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 になる
- フラグメントの field が受け取る引数を定義する
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
}

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.

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

-
Relay における Mutation
- useMutation を使う
- サーバーに送る変数に ID が含まれている場合、Relay がいい感じにレスポンスから store を更新してくれるので、手動で store の値を更新しに行く必要はない
- これはMutationのレスポンスを受けてからデータを更新する
- そのため、更新までの待ち時間が存在する
- Loading を表示することもある
-
Mutation のリクエストのレスポンスを待たないで更新したい
- たとえば、Post にコメントを書いて、投稿ボタンを押す、レスポンスがあるまで待つんじゃなくて、画面上は反映、リクエストが失敗したときに戻すってイメージ
- Relay はこの辺の仕組みも持っている
- optimistic update: 楽観的更新
- ★ 更新されるでしょうって前提で進めるってかんじだと思う
- 楽観的な更新で、更新したい情報をローカルデータストアに反映する
- その後、サーバーにリクエストを実行する
- サーバーからのレスポンスを待って
- 成功の場合: サーバーからのレスポンスを使ってローカルデータストアを更新する
- 失敗した場合: 最初の状態に戻る
- optimistic update: 楽観的更新