🚪

Next.js, Rails環境でRelayにサクッと入門してみた

2022/12/02に公開

はじめに

この記事はCODEBASE OKINAWA Advent Calendar 20221日目の記事になっております。今回は普段触っていなかったGraphQLクライアントの一つであるRelayにサクッと入門したので、やっていく中で詰まった内容や環境構築の手法について書いていこうと思います。

https://adventar.org/calendars/7795

また今回実際に試した内容はGitHubに上げております

https://github.com/puremoru/nextjs-rails-relay-sample

Relayとは

RelayはMeta社が開発しているGraphQLクライアントで、同列にApollo Clientやurqlなどがあります。
Apollo Clientなどと同様にnormalized cacheの機構があり、通信効率の良い状態管理ができます。
また、Relayは開発元がMeta社だけあり、通信状態がSuspenseやErrorBoundaryを使う様になっており、他のGraphQLクライアントに比べてReactファーストな思想が強いと感じました。

筆者個人としては、元々Apollo Clientを使って開発することが多かったのですが、RelayはFragment Collocationを強制することにより、堅牢性の高いコンポーネント設計がしやすいことから、より大規模なアプリケーション向きの印象のあったRelayを今回手っ取り早く試したいと思ったので記事にすることにしました。

主な使用技術

  • React 18.2.0
  • Next.js 13.0.5
  • TypeScript 4.9.3
  • Ruby 3.1.2
  • Rails 7.0.4
  • Relay
  • graphql-ruby

Railsでgraphql-ruby環境を構築する

まずはRailsでGraphQL環境を作っていきます。RailsでのGraphQL環境構築は多くの方が投稿しているのでここではある程度省略したいと思います。

https://zenn.dev/slowhand/articles/4fe99377185100

https://zenn.dev/necocoa/articles/setup-graphql-ruby

https://zenn.dev/kei178/articles/2f4ffc6b89618c

GraphQL環境が構築できて以下のようにPlaygroundでtestFieldのqueryを実行できる様になっているか確認します。

Next.jsから実行するためのQueryを実装する

今回は手っ取り早く試すので、超簡単に以下のカラムを持ったpostsテーブルを用意します。

id: ID
content: String
like_count: Integer
created_at: datetime
updated_at: datetime
% rails g model Post content:string like_count:integer                       
      invoke  active_record
      create    db/migrate/20221129160525_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml

seeds.rbも適当に作る

db/seeds.rb
Post.create([{ content: 'test', like_count: 20 }, { content: '2022年もいい年でしたね👶', like_count: 2022 }])

生成したPostモデルをもとにGraphQLのTypes::PostTypeを生成します。

% rails g graphql:object post                                                
      create  app/graphql/types/post_type.rb

次に実際にpostsを全件取得するQueryを追加する

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    # Add root-level fields here.
    # They will be entry points for queries on your schema.

    # TODO: remove me
    field :test_field, String, null: false,
                               description: 'An example field added by the generator'
    def test_field
      'Hello World!'
    end
    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

次にNext.jsからGraphQLのAPIをリクエストできるようにCORSの設定をするために以下のgemを追加する

Gemfile
gem 'rack-cors'

installしたらconfig/initializers/cors.rbに以下の内容を加える
(今回はNext.jsが3000, Railsが4000ポートを使うことにしています。)

config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3000'

    resource '*',
    headers: :any,
    methods: [:get, :post, :put, :patch, :delete, :options, :head],
    credentials: true
  end
end

これで一旦Rails側の実装は終わりなのでNext.js, Relayの方に進んでいく。

Next.jsでRelayの環境を構築する

通常通り、以下のコマンドでNext.jsのプロジェクトを作成する

% yarn create next-app --typescript

続いてRelayを動かすために必要なライブラリのインストールを行う

% yarn add relay-runtime react-relay
% yarn add --dev relay-compiler graphql babel-plugin-relay @types/react-relay relay-config

次にいくつかRelayを動かすための設定ファイルの変更、追加をしていきます
まずはrelay.config.jsを作成。今回はTypeScriptを使った環境なのでtypescriptを指定しています。

relay.config.js
module.exports = {
  src: "./src",
  language: "typescript", // "javascript" | "typescript" | "flow"
  schema: "./schema.graphql",
  exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"],
}

babelの設定にもRelayを使うようにpluginに追加をします

.babelrc
{
  "plugins": ["relay"],
  "presets": ["next/babel"]
}

次に実際にRailsで立ち上げているGraphQLサーバーへリクエストするための環境の設定ファイルを追加します。

lib/client_environment.js
import { useMemo } from 'react'
import { Environment, Network, RecordSource, Store } from 'relay-runtime'

let relayEnvironment

function fetchQuery(operation, variables) {
  return fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: operation.text,
      variables,
    }),
  }).then((response) => response.json())
}

export function createEnvironment() {
  return new Environment({
    network: Network.create(fetchQuery),
    store: new Store(new RecordSource()),
  })
}

export function initEnvironment(initialRecords) {
  const environment = relayEnvironment ?? createEnvironment(initialRecords)

  if (initialRecords) {
    environment.getStore().publish(new RecordSource(initialRecords))
  }
  if (typeof window === 'undefined') return environment
  if (!relayEnvironment) relayEnvironment = environment

  return relayEnvironment
}

export function useEnvironment(initialRecords) {
  const store = useMemo(() => initEnvironment(initialRecords), [initialRecords])
  return store
}
NEXT_PUBLIC_GRAPHQL_ENDPOINT=http://localhost:4000/graphql

relay compierを実行するためにpackage.jsonのscriptsに以下を追加する

package.json
"relay": "relay-compiler"

次にRelay CompilerでQueryの型生成をするためにGraphQLサーバーのschemaをNext.js側で保持する必要があります。
そのために再度Rails側に戻ります。

こちらのコメントを参考にGraphQLのschemaファイルを生成するRakeタスクを追加する。
https://github.com/rmosolgo/graphql-ruby/issues/2501#issuecomment-536199861

lib/tasks/graphql.rake
require "graphql/rake_task"
GraphQL::RakeTask.new(schema_name: "RelaySampleBackendSchema") # ここのschema名は自分の環境のschema名を確認する

そして以下のコマンドを実行する

% rails graphql:schema:dump

実行するとschema.graphqlというファイルが生成されるのでこのファイルの内容をNext.jsの /data に追加する
(この手順、もっと簡単な方法がありそう)

次に今回Next.js側で実行するQueryを定義する

src/queries/posts.query.ts
import { graphql } from "react-relay";

const PostsQuery = graphql`
  query postsQuery {
    posts {
      id
      content
      likeCount
    }
  }
`;

export default PostsQuery;

ここまでの設定をした後で以下のコマンドでrelay compilerを実行する

% yarn relay                                                                                                                                                                                           (git)-[main]
yarn run v1.22.19
warning ../../../../package.json: No license field
$ relay-compiler
[INFO] [default] compiling...
[INFO] [default] compiled documents: 1 reader, 1 normalization, 1 operation text
[INFO] Done.
✨  Done in 0.38s.

これで定義したQueryの型が生成されたので取得したpostsを表示するpageを作っていく
_app.tsxにはRelayのProviderを読み込んでおく

pages/_app.tsx
import "../styles/globals.css";
import { AppProps } from "next/app";
import { RelayEnvironmentProvider } from "react-relay";
import { useEnvironment } from "../lib/client_environment";

export default function App({ Component, pageProps }: AppProps) {
  const environment = useEnvironment(pageProps.initialRecords);
  return (
    <RelayEnvironmentProvider environment={environment}>
      <Component {...pageProps} />
    </RelayEnvironmentProvider>
  );
}
pages/posts/index.tsx
import React, { Suspense } from "react";
import { useLazyLoadQuery } from "react-relay/hooks";
import PostsQuery from "../../src/queries/posts.query";
import { postsQuery } from "../../src/queries/__generated__/postsQuery.graphql";

const PostsIndexPage = () => {
  const { posts } = useLazyLoadQuery<postsQuery>(PostsQuery, {});
  return (
    <>
      <Suspense fallback="Loading...">
        {posts.map((post) => {
          return (
            <div key={post.id}>
              <p>id: {post.id}</p>
              <p>content: {post.content}</p>
              <p>likes: {post.likeCount}</p>
            </div>
          );
        })}
      </Suspense>
    </>
  );
};

export default PostsIndexPage;

実際にページにアクセスして以下の内容が表示されていれば無事Relayを使ってGraphQLサーバーからデータ取得ができている。

まとめ

relayはgraphql-codegenなどApolloを使う場合に併用するようなライブラリの機能も全て含んでいるライブラリなので設定でやることが多い様に思います。何か参考になれば幸いです。
今回手っ取り早くRelayに入門するために簡単なQueryを実行するまでを試してみましたが、mutationや認証ありのリクエスト実行などがまだ試せていないので随時記事として更新できたらと思います。

AppBrew

Discussion