🏂

Next.js × Relay でGraphQLフロントエンド環境を構築してGitHub APIにアクセスする

2021/08/16に公開3

私が開発・運営しているスノーアクティビティイベントマッチングサービス「snowwshiro」に、Next.js × Relayを使ったGraphQLフロントエンド環境を構築しようとしたところ、かなり手間取りましたので、記事にしました。

最終的なコードについては、以下にて公開しています。

https://github.com/snowwshiro/nextjs-relay-demo

前提

現在snowwshiroは、フロントエンド・バックエンド共にRuby on Railsにて開発しています。

一方、私自身は現在他プロジェクトでフロントエンドはReact・Next.js、バックエンドはFirebaseを使って開発する機会が多く、snowwshiroの開発も出来ればフロントエンドはNext.jsを使いたいという気持ちが高まっていました。
バックエンドもFirebaseに移行することを検討しましたが、現状のロジックを全てFirestore・Functionsで再構築することに躊躇がありました。但し、認証については現在使用しているdeviseからFirebase Authenticationに移行したいと考えました。
上記を踏まえ、現在は以下方針にて移行作業を進めています。

  • フロントエンドはNext.js、バックエンドはRails APIモードを利用する
  • フロンド・エンド間の通信には、GraphQLを用いる
  • 認証をdeviseからFirebase Authenticationに移行する

本記事では、Next.js × Relayを使ったGraphQLフロントエンド環境を構築し、簡単なQueryで取得したデータを表示するところまでをご紹介します。

用語紹介

Next.js

Next.jsは、Reactベースのフレームワークです。公式サイトでは以下のように紹介されています。

Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.

https://nextjs.org/

GraphQL

GraphQLは、Web API上で動くクエリ言語とエンジンの仕様です。2012年にFacebookが開発し、2015年にオープンソース化、2019年以降はGraphQL財団が設立されています。

https://graphql.org/

Relay

Relayは、Facebookが開発しているReact向けGraphQLクライアントツールです。

https://relay.dev/

GraphQL自体は単純なhttpリクエストで利用することが可能ですが、クライアントツールにより様々な機能を利用することが可能となります。
Relay以外のクライアントツールとしてはApollo Clientが有名で、記事検索でもApolloの事例が多い(その分今回のRelay導入は苦戦した)のですが、私は以下記事を参考にし、Next.jsでのSSGとの相性を踏まえてRelayを用いることにしました。

https://wawoon.dev/posts/relay-modern-with-nextjs

手順

筆者の開発環境は以下となります。

Mac mini (2018) Intel
macOS Big Sur 11.5

Next.jsのセットアップ

ターミナルで以下コードを実行します。今回はTypeScriptもセットします。

yarn create next-app --typescript

セットアップ中に以下のようにプロジェクト名をどうするか聞かれるので、今回は「nextjs-relay-demo」としました。

What is your project named? … nextjs-relay-demo

プロジェクトディレクトリに移動して、Next.jsを立ち上げてみます。

cd nextjs-relay-demo
yarn dev

http://localhost:3000 に、以下画面が立ち上がりました。

このまま作業を進めても問題ありませんが、最低限の整理として、ディレクトリを以下の通りとしました。
(srcディレクトリを作成し、その中にpages、stylesを移動、さらにcomponentsとlibを新たに作成しました)

Relayを使わずにGraphQLにフェッチする

Relayを導入する前に、GraphQLサーバーにHTTPリクエストでアクセスしてみます。
今回、GraphQLサーバーにはGitHub GraphQL APIを使用します。
(ご自身のGitHubアカウントが必要となります。)

GitHub トークンを作成する

https://github.com/settings/tokens を開き、Generate new token を押して、トークンを作成します。

nextjs-relay-demo/.env.localを作成し、以下のように保存します

NEXT_PUBLIC_GITHUB_AUTH_TOKEN=生成されたトークン

fetchヘルパーを作り、リポジトリ名を表示する

Relayを導入する前に、シンプルなリクエストでデータを取得してみます。

libディレクトリ内にfetchヘルパーを作成します。

src/lib/fetchGraphQL.js
async function fetchGraphQL(text, variables) {
  const Token = process.env.NEXT_PUBLIC_GITHUB_AUTH_TOKEN;
  const response = await fetch('https://api.github.com/graphql', {
    method: 'POST',
    headers: {
      Authorization: `bearer ${Token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: text,
      variables,
    }),
  });

  return await response.json();
}

export default fetchGraphQL;

pagesのindex.tsの中身を全削除し、以下のように書き換えます。

src/pages/index.ts
import React, { useState, useEffect } from 'react';
import fetchGraphQL from '../lib/fetchGraphQL';

const Home = () => {
  const [name, setName] = useState(null);

  useEffect(() => {
    let isMounted = true;
    fetchGraphQL(`
      query RepositoryNameQuery {
        repository(owner: "snowwshiro" name: "snowwshiro") {
          name
        }
      }
    `).then(response => {
      if (!isMounted) {
        return;
      }
      const data = response.data;
      setName(data.repository.name);
    }).catch(error => {
      console.error(error);
    });

    return () => {
      isMounted = false;
    };
  }, [fetchGraphQL]);

  return (
    <p>{name != null ? `Repository: ${name}` : "Loading"}</p>
  )
}

export default Home

yarn dev で立ち上げると、以下のように表示されました。

Relayを導入する

上記のfetchヘルパー及びこれからのRelayの導入手順は、以下Relayの公式手順を参考にしました。

https://relay.dev/docs/getting-started/step-by-step-guide/

上記公式はReactプロジェクト向けの手順なので、Next.jsのSSG等に対応した設定については、以下Next.js公式のRelay事例を参考にしました。

https://github.com/vercel/next.js/tree/canary/examples/with-relay-modern

Relayをインストールする

ターミナルにて、以下コマンドでRelay及び関連パッケージをインストールします。

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

設定ファイルを作成する

Next.js用に設定ファイルを以下の通り作成します。

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

let relayEnvironment

function fetchQuery(operation, variables, cacheConfig, uploadables) {
  const Token = process.env.NEXT_PUBLIC_GITHUB_AUTH_TOKEN;
  return fetch(process.env.NEXT_PUBLIC_RELAY_ENDPOINT, {
    method: 'POST',
    headers: {
      Authorization: `bearer ${Token}`,
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: operation.text,
      variables,
    }),
  }).then((response) => response.json())
}

function createEnvironment(initialRecords) {
  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
}
.babelrc
{
  "presets": ["next/babel"],
  "plugins": ["relay"]
}
.graphqlconfig
{
  "schemaPath": "src/schema/schema.graphql",
  "extensions": {
    "endpoints": {
      "dev": "https://api.github.com/graphql"
    }
  }
}

.env.localに以下を追加します

NEXT_PUBLIC_RELAY_ENDPOINT=https://api.github.com/graphql

Relay compilerを設定する

コンパイラーの設定の前に、ターミナルにて以下コマンドを実行しスキーマファイルを取得します。

curl https://raw.githubusercontent.com/relayjs/relay-examples/main/issue-tracker/schema/schema.graphql > schema.graphql

取得したスキーマファイルは、src配下にschemaディレクトリを作成し、そちらに移しておきます。

packages.jsonの"scripts"に、以下を追加します。

"relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**'  '**/__generated__/**' --exclude '**/schema/**' --schema ./src/schema/schema.graphql --extensions js jsx ts tsx"

Relayを使ってリポジトリ名を表示する

src内にqueriesディレクトリを作り、以下ファイルを作成します。

src/queries/repository.ts
import { graphql } from 'react-relay'

export default graphql`
  query repositoryQuery {
    repository(owner: "snowwshiro" name: "snowwshiro") {
      name
    }
  }
`

ターミナルで以下コマンドを実行します。

yarn run relay

queriesディレクトリ内に__generated__ディレクトリが自動で作成され、その中にrepositoryQuery.graphql.jsファイルが自動で作成されます。

pagesディレクトリ内の_app.tsxを以下のように書き換えます。

src/pages/_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { ReactRelayContext } from 'react-relay'
import { useEnvironment } from '../lib/relay'

function MyApp({ Component, pageProps }: AppProps) {
  const environment = useEnvironment(pageProps.initialRecords)
  return (
    <ReactRelayContext.Provider value={{ environment, variables: {} }}>
      <Component {...pageProps} />
    </ReactRelayContext.Provider>
  )
}
export default MyApp

pagesディレクトリ内のindex.tsxを以下のように書き換えます。

src/pages/index.tsx
import React from 'react';
import { fetchQuery } from 'react-relay'
import { initEnvironment } from '../lib/relay'
import repositoryQuery from '../queries/repository'

export async function getServerSideProps() {
  const environment = initEnvironment()
  const queryProps = await fetchQuery(environment, repositoryQuery, {}).toPromise()
  return {
    props: {
      ...queryProps,
    },
  }
}

export default function Home(props) {
  return (
    <p>{props != null ? `Repository: ${props.repository.name}` : "Loading"}</p>
  )
}

yarn dev で立ち上げると、以下のように表示されました。

あとがき

今回はNext.js・Relayでデータ取得・表示までを行いましたが、ここに至るまで相当な設定ミス・エラーを経験しました。同じような経験をされている方の参考となれば幸いです。
また、型定義が不十分なことと、Relay本来の機能を活用した形に出来ていないので、今後改善して記事の続編にしたいと考えています。
あと、Firebase Authとの連携等も、整理が出来た段階で記事にする予定です。

Discussion

りんたろーりんたろー

質問失礼します。
こちらの記事を拝見させてもらいながら環境を立てていたのですが最後のindex.tsxにおいてfetchQueryをした際の返り値がunknown型となってしまいます。解決策など何かございましたらお願いします。

りんたろーりんたろー

なるほど。やはりunknownで返ってくる際はタイプアセーション使わないとだめですよね、、、