Relayはnodeクエリを発行することがあるらしい

## 本記事のまとめ
- Relay GraphQL Clientが`node`クエリを利用し、NodeResolverをコールするタイミングを明らかにした。
- `@refetchable`を利用すると、relay-compilerが、`node`クエリを利用するようなクエリを生成することがある。
- RefetchableFragmentを利用しない場合、`node`クエリと、NodeResvolerは不要になる(と思われる)。


## 背景

`Node`は、グローバルに一意なIDを持つインターフェースのことです。
以下のように、GraphQLクライアントがキャッシュやデータの再フェッチを行うために、GraphQLの各オブジェクトに実装しておくことが推奨されます。

```graphql
interface Node {
  id: ID!
}

# Nodeを実装したオブジェクトの例
type MyObject implements Node {
  id: ID!
  name: String!
}
```

このことは、以下のページにも書かれています。

https://graphql.org/learn/global-object-identification/

ただし、`Node`を実装しただけでは不十分で、以下のように、スキーマに`node`クエリを指定する必要があるようです[^1]。また、クエリがあるということはResolverも当然実装する必要があります(サーバ側の話なので、ここではResolverの内部実装ついては触れません)。

```graphql
type Query {
  node(id: ID!): Node
}
```

[^1]: [GraphQL Server Specification](https://relay.dev/docs/next/guides/graphql-server-specification/#object-identification)では、`node`クエリが実装されているため、筆者が必要になると判断した。Relay本家が明言しているわけではない。


しかし、そこで1つ疑問が生じます。通常クライアント側に

```graphql
query PageQuery {
  myObject {
    id
    name
  }
}
```

のようなコードは書くことがあっても、

```graphql
query PageQuery($id: ID!) {
  node(id: $id) {
    name
  }
}
```

のように、直接`node`クエリを書くことは基本的にないのではないでしょうか。そこで、いつどのタイミングでこの`node`クエリが発行されるのか、もしくは実は発行されないのか、気になったため調べてみました。


## 調査したこと

まず、無邪気にRelayの質問ページから質問を投げてみました。

https://relay.dev/help/

すると、以下のページを案内されました。

https://relay.dev/docs/tutorial/refetchable-fragments/

ページを確認したところ、`refetch`の時に使用されることはわかったのですが、正直確証を得るまでには至りませんでした。そこで、実際にサンプルを動かしてみることにしました。セットアップが手間なので、Next.jsを使ってサンプルを作ります。

まず、`pages/index.tsx`に、クエリ本体とRefetchableなクエリを用意します。事前にRelayとNext.jsのセットアップは済ませてあるものとします。

```ts
const query = graphql`
  query pagesIndexQuery {
    myObject {
      id
      name
      ...pagesIndexFragment
    }
  }
`

const fragment = graphql`
  fragment pagesIndexFragment on MyObject 
    @refetchable(queryName: "RefetchQuery") {
      id
      name
  }
`
```

ここで、`pnpm build`を行い、自動生成された`RefetchQuery.graphql.ts`を確認してみます。120行目あたりの`params.text`を確認すると確かに`node`クエリが生成されていることが確認できました。

```
"query RefetchQuery(\n  $id: ID!\n) {\n  node(id: $id) {\n    __typename\n    ...pagesIndexFragment (以下省略)
```

続いて、コンポーネントやサーバも作成し、実際にNodeResolverが叩かれるかを確認してみます。先ほどの `pages/index.tsx`以下のように修正します。

```tsx
import { graphql, useLazyLoadQuery, useRefetchableFragment } from "react-relay"
import { pagesIndexQuery } from "@/queries/__generated__/pagesIndexQuery.graphql"

const query = graphql`
  query pagesIndexQuery {
    myObject {
      id
      name
      ...pagesIndexFragment
    }
  }
`

const fragment = graphql`
  fragment pagesIndexFragment on MyObject @refetchable(queryName: "RefetchQuery") {
    id
    name
  }
`

export default function Index() {
  const data = useLazyLoadQuery<pagesIndexQuery>(query, {})
  const [_, refetch] = useRefetchableFragment(fragment, data.myObject)
  
  const onClick = () => {
    refetch({})
  }
  
  return (
    <main>
      <div>{JSON.stringify(data)}</div>
      <button onClick={onClick}>
        refetch
      </button>
    </main>
  )
}

```

画面はこんな感じですね。

![スクリーンショット 2023-12-03 20.51.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/9460baa7-f3bc-b965-d36b-1df5f303742e.png)

https://my8xkv-3001.csb.app/

早速叩いてみます。
ページロード時はPageクエリが叩かれるため`node`は利用されません

![スクリーンショット 2023-12-03 20.49.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/670ccf52-3148-c128-4b2a-d09bb3253717.png)

が、更新ボタン押下時に行われるrefetchの場合は`node`クエリが発行され、NodeResolverが利用されていることが確認できました。

![スクリーンショット 2023-12-03 20.49.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/3fe11cd3-487b-39d9-cce3-5e6626b7c0d6.png)


サンプルコードはこちらです。

- [Sample Server](https://codesandbox.io/p/devbox/relay-refetch-example-server-rwhr36)
- [Sample Client](https://codesandbox.io/p/devbox/relay-refetch-example-client-my8xkv)


## まとめ (再掲)

- Relay GraphQL Clientが`node`クエリを利用し、NodeResolverをコールするタイミングを明らかにした。
- `@refetchable`を利用すると、relay-compilerが、`node`クエリを利用するようなクエリを生成することがある。
- RefetchableFragmentを利用しない場合、`node`クエリと、NodeResvolerは不要になる(と思われる)。

他にもこのケースで`node`クエリが発行されることがあるよ、という情報をお持ちの方はぜひ共有いただけると幸いです。