Apollo + TypeScript ユーザが Relay を学ぶ
モチベーション
...
触ってみた感想
...
Getting Started > Step-by-step Guide を見ていく
- Step 1~2 は Relay なしの実装
- Step 4 から Relay を入れ始める
relay-compiler の TypeScript 対応は Guides > Type Emission にあった
$ yarn add --dev relay-compiler-language-typescript @types/react-relay @types/relay-runtime $ relay-compiler --language typescript […]
import graphql from "babel-plugin-relay/macro";
で型定義がなくてエラーになるのは、自分で定義するしかないらしい
declare module "babel-plugin-relay/macro" { export { graphql as default } from "react-relay"; }
Next.js 上で試しているが、とりあえず src/types/babel-plugin-relay.d.ts
に置いておく
疑問
-
Environment
作成やloadQuery
を React の外でやってるが、実際どうやるのがいいのか?- 実アプリを考えると、
- 前者は SSR での Hydration で initial state を注入する必要があるため、ライフサイクルは React 上で管理したい
- 後者は複数ページ持つアプリのときにルーティングと独立して loadQuery されることになるので論外なはず
- 実アプリを考えると、
Next.js に relay のサンプルあるけど、SSG 前提になってて微妙に参考にならない
babel-plugin-macros 入れ忘れててエラーになった
それはそう
{
"presets": ["next/babel"],
"plugins": ["macros"]
}
Guides > Rendering Data Basics 以下を見ていく
まずは Queries
usePreloadedQuery
の説明とか書いている
さっきの「loadQuery
どこでやるんや問題」については、useQueryLoader
を使うといいらしい
-
useQueryLoader
はqueryRef
(PreloadedQuery
)とloadQuery
を返す -
loadQuery
を呼ぶとqueryRef
に値が入る -
queryRef
をusePreloadedQuery
に渡すと結果を取り出せる
型的に useQueryLoader
と usePreloadedQuery
は同一コンポーネント内で呼び出すことはできない。
Apollo の useQuery
に比べるとかなりまどろっこしい印象になる。
一方で、GraphQL を使っていて「1ページ内で複数クエリが飛ぶ」ということ自体何か間違っている可能性がある。
そう考えるとクエリ実行に気軽さを持ち込むということが設計ミスを誘発させるデザインなのかもしれない(原理主義)
適当に作ってたら relay-compiler に怒られた
Parse error: Error: RelayFindGraphQLTags: Operation names in graphql tags must be prefixed with the module name and end in "Mutation", "Query", or "Subscription".
クエリ名の Prefix とモジュール名(コンポーネントのディレクトリ名コンポーネントを定義するファイル名)は一致させる必要があるらしい
レールを感じる
突然 The above error occurred in the <Home> component:
とか言われて above にエラーないしとか思っておもむろに ErrorBoundary 挟んだら <Suspence>
入れろって出てきた
useQueryLoader
か usePreloadedQuery
が Suspence 前提なのか
適当にログ挟んでみた感じだと、usePreloadedQuery
が一瞬 throw してそう
ただ、Next.js で Suspense いれると ReactDOMServer does not yet support Suspense.
になる :thinking:
Next.js はページ遷移時でもちゃんと getServerSideProps
を呼んでくれるので、そもそもクエリ実行をフロントでやる必要がないのか
Next.js 側の例のように、getServerSideProps
で fetchQuery
して結果をそのままレンダリングする
fetchQuery が返すのは PreloadedQuery じゃないので問題にならない
次、Fragments を見る
The main building block for declaring data dependencies for React Components in Relay are GraphQL Fragments.
useFragment
の第1引数に DocumentNode を渡す
第2引数は省略した状態で relay-compiler を実行すると、なんかいい感じの型(<fragment_name>$key
)が生成されるので、それを渡す
type Props = {
user: UserComponent_user$key,
};
function UserComponent(props: Props) {
const data = useFragment(
graphql`
fragment UserComponent_user on User {
name
profile_picture(scale: 2) {
uri
}
}
`,
props.user,
);
fragment の masking をフレームワークでサポートしてくれるのはありがたい
(昔は HOC 的な感じだった気がするんだけど、いつの間にかフル Hooks ベース API になってる)
Fragment 名もクエリと同じで、モジュール名(コンポーネントを定義してるファイル名)を prefix として求めてくる
かつ、global に unique である必要もある
relay では <module_name>_<property_name>
を推奨してるとのこと
Fragment names need to be globally unique. In order to easily achieve this, we name fragments using the following convention based on the module name followed by an identifier:
<module_name>_<property_name>
. This makes it easy to identify which fragments are defined in which modules and avoids name collisions when multiple fragments are defined in the same module.
Relay は(Facebook では)ファイル名はプロジェクト内でユニークであることを推奨している?
ディレクトリが全フラットだったりするのかな?
親コンポーネントの Fragment や Query では子コンポーネントの Fragment を spread しておくだけで、いい感じに合成される
便利
const data = usePreloadedQuery<AppQuery>(
graphql`
query AppQuery($id: ID!) {
user(id: $id) {
name
# Include child fragment:
...UserComponent_user
}
}
`,
appQueryRef,
);
ちょっと飛んで Connections(Pagination) 周りを見る
ふつうの useFragment
との違い
-
usePaginationFragment
を使う - fragment に
@refetchable
をつける- この時点ではつけられる条件しか書かれておらず、なぜ必要なのかはわからない
- connection フィールドに
@connection
をつける- これも Why は不明
- (型見たら Connection だとわかりそうだし、だったら勝手にやってくれよと思わなくもない)
-
<SomeName>_<fieldName>
でないと relay-compiler に怒られる- (なので
<FragmentName>_<fieldName>
にしとけばよさそう)
- (なので
- これも Why は不明
-
first
とafter
はスキーマ的に nullable でも、省略すると relay-compiler に怒られる
loadNext
may cause the component or new children components to suspend (as explained in Loading States with Suspense). This means that you'll need to make sure that there's aSuspense
boundary wrapping this component from above.
とのことだが、List や ListItem を Suspense で囲んで loadNext
しても、 fallback component は出なかった
なんでだろう
(いきなり Next.js と組み合わせたせいで切り分けがムズい)
usePaginationFragment
の第2型引数は Flow だと _
でいいらしいが、TypeScript だと <FragmentName>$key
を明示的に渡す必要があるっぽい
Fragment で引数を使うので @argumentDefinitions
directive があったほうがいい気がするんだけど、その節を読み飛ばしたせいでよくわかっていない
@streaming_connection
directive を使うと、first view のコンテンツ取得と load more を @defer
/ @stream
でうまい感じにやってくれるらしい
後で試す
Next.js で Suspense
使うためには experimental.concurrentFeatures
を有効化する必要があるが、 relay-runtime とのかみ合わせが悪いらしい
Test や Storybook などでの利用について
Apollo や urql はあくまで "ライブラリ" だが、Relay は opinionated なフレームワークである というのが本質的な違いだと理解した
Relay はクエリの名前・クエリの配置・コンポーネントの配置にまで口を出してくるが、そのルールに従っていると生産性高いコードが書ける
ただ、与えている制約が強いので既存コードベースに Relay を突っ込むのは大変だろうなという印象。