Closed27

Apollo + TypeScript ユーザが Relay を学ぶ

izuminizumin

Getting Started > Step-by-step Guide を見ていく

  • Step 1~2 は Relay なしの実装
  • Step 4 から Relay を入れ始める
izuminizumin

relay-compiler の TypeScript 対応は Guides > Type Emission にあった

$ yarn add --dev relay-compiler-language-typescript @types/react-relay @types/relay-runtime
$ relay-compiler --language typescript []
izuminizumin

import graphql from "babel-plugin-relay/macro"; で型定義がなくてエラーになるのは、自分で定義するしかないらしい

declare module "babel-plugin-relay/macro" {
  export { graphql as default } from "react-relay";
}

https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35707

Next.js 上で試しているが、とりあえず src/types/babel-plugin-relay.d.ts に置いておく

izuminizumin

疑問

  • Environment 作成や loadQuery を React の外でやってるが、実際どうやるのがいいのか?
    • 実アプリを考えると、
      • 前者は SSR での Hydration で initial state を注入する必要があるため、ライフサイクルは React 上で管理したい
      • 後者は複数ページ持つアプリのときにルーティングと独立して loadQuery されることになるので論外なはず
izuminizumin

babel-plugin-macros 入れ忘れててエラーになった
それはそう

{
  "presets": ["next/babel"],
  "plugins": ["macros"]
}
izuminizumin

Guides > Rendering Data Basics 以下を見ていく

まずは Queries

izuminizumin

usePreloadedQuery の説明とか書いている
さっきの「loadQuery どこでやるんや問題」については、useQueryLoader を使うといいらしい

  • useQueryLoaderqueryRef(PreloadedQuery)と loadQuery を返す
  • loadQuery を呼ぶと queryRef に値が入る
  • queryRefusePreloadedQuery に渡すと結果を取り出せる

型的に useQueryLoaderusePreloadedQuery は同一コンポーネント内で呼び出すことはできない。
Apollo の useQuery に比べるとかなりまどろっこしい印象になる。
一方で、GraphQL を使っていて「1ページ内で複数クエリが飛ぶ」ということ自体何か間違っている可能性がある。

そう考えるとクエリ実行に気軽さを持ち込むということが設計ミスを誘発させるデザインなのかもしれない(原理主義)

izuminizumin

適当に作ってたら 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 とモジュール名(コンポーネントのディレクトリ名コンポーネントを定義するファイル名)は一致させる必要があるらしい
レールを感じる

izuminizumin

突然 The above error occurred in the <Home> component: とか言われて above にエラーないしとか思っておもむろに ErrorBoundary 挟んだら <Suspence> 入れろって出てきた
useQueryLoaderusePreloadedQuery が Suspence 前提なのか

izuminizumin

適当にログ挟んでみた感じだと、usePreloadedQuery が一瞬 throw してそう
ただ、Next.js で Suspense いれると ReactDOMServer does not yet support Suspense. になる :thinking:

izuminizumin

Next.js はページ遷移時でもちゃんと getServerSideProps を呼んでくれるので、そもそもクエリ実行をフロントでやる必要がないのか
Next.js 側の例のように、getServerSidePropsfetchQuery して結果をそのままレンダリングする
fetchQuery が返すのは PreloadedQuery じゃないので問題にならない

izuminizumin
izuminizumin

The main building block for declaring data dependencies for React Components in Relay are GraphQL Fragments.

izuminizumin

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 になってる)

izuminizumin

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 では)ファイル名はプロジェクト内でユニークであることを推奨している?
ディレクトリが全フラットだったりするのかな?

izuminizumin

親コンポーネントの Fragment や Query では子コンポーネントの Fragment を spread しておくだけで、いい感じに合成される
便利

  const data = usePreloadedQuery<AppQuery>(
    graphql`
      query AppQuery($id: ID!) {
        user(id: $id) {
          name

          # Include child fragment:
          ...UserComponent_user
        }
      }
    `,
    appQueryRef,
  );
izuminizumin

ちょっと飛んで Connections(Pagination) 周りを見る
https://relay.dev/docs/guided-tour/list-data/connections/

izuminizumin

ふつうの useFragment との違い

  • usePaginationFragment を使う
  • fragment に @refetchable をつける
    • この時点ではつけられる条件しか書かれておらず、なぜ必要なのかはわからない
  • connection フィールドに @connection をつける
    • これも Why は不明
      • (型見たら Connection だとわかりそうだし、だったら勝手にやってくれよと思わなくもない)
    • <SomeName>_<fieldName> でないと relay-compiler に怒られる
      • (なので <FragmentName>_<fieldName> にしとけばよさそう)
  • firstafter はスキーマ的に nullable でも、省略すると relay-compiler に怒られる

https://relay.dev/docs/guided-tour/list-data/rendering-connections/

izuminizumin

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 a Suspense boundary wrapping this component from above.

とのことだが、List や ListItem を Suspense で囲んで loadNext しても、 fallback component は出なかった
なんでだろう
(いきなり Next.js と組み合わせたせいで切り分けがムズい)

izuminizumin

usePaginationFragment の第2型引数は Flow だと _ でいいらしいが、TypeScript だと <FragmentName>$key を明示的に渡す必要があるっぽい

izuminizumin

Fragment で引数を使うので @argumentDefinitions directive があったほうがいい気がするんだけど、その節を読み飛ばしたせいでよくわかっていない

izuminizumin

Apollo や urql はあくまで "ライブラリ" だが、Relay は opinionated なフレームワークである というのが本質的な違いだと理解した
Relay はクエリの名前・クエリの配置・コンポーネントの配置にまで口を出してくるが、そのルールに従っていると生産性高いコードが書ける
ただ、与えている制約が強いので既存コードベースに Relay を突っ込むのは大変だろうなという印象。

このスクラップは2022/09/24にクローズされました