📚

supabase + GraphQL + Next.js + React18 で SSR 対応 Todo を作る

2022/04/18に公開

・ソースコード
https://github.com/SoraKumo001/supabase-test01

・Vercel での動作確認
https://supabase-test01.vercel.app/

・カスタム版 supabase-cli
https://github.com/SoraKumo001/supabase-cli.old/releases

・こちらでも同じ記事を書いています
https://next-blog.croud.jp/contents/eyEtMbx4TqVeF7NuuSAR

1.Supabase の GraphQL 対応

Firebase の代替として候補に挙がることの多い https://supabase.com/ が 2022/03/29 に GraphQL に対応しました。新規でプロジェクトを作るとエンドポイント/graphql/v1 から GraphQL の機能を利用可能となっています。

2.supabase-cli でローカル環境を整える

ということで早速使っていきたいと思います。まずはsupbase-cliを使って、ローカルに開発環境を作っていきましょう。

2.1 なんだと、動かん

supbase-cli をインストールし supabase start 実行後、拡張機能 pg_graphql を有効にしても、/graphql/v1のエンドポイントが正常に機能していません。issues を見ても、特にこれと言って何も見つかりませんでした。仕方が無いのでソースコードを fork して根本的な原因を調査しました。

2.2 色々バグってる

  • supabase/postgres のバージョンが低くて graphql.resolve が存在しない
  • kong のエンドポイントの設定で余計なスラッシュがついており、目的の場所へルーティングされない
  • graphql_public スキーマが存在しておらず、GraphQL の機能を利用するために必要な graphql_public.graphql ファンクションが無い

ぶっちゃけ動くわけが無いので、足りない機能を supbase-cli に追加しました

2022/04/15 時点で公式版で動くようになりました。
最終的に GraphQL のエンドポイントだけは修正されなかったので、こちらでプルリクを出して直してもらいました。

2.3 supabase-cli によるローカル環境の起動

2.3.1 supabase の起動

起動は以下のコマンドになります。

supabase init
supabase start

以下の内容が表示され起動します

         API URL: http://localhost:54321
          DB URL: postgresql://postgres:postgres@localhost:54322/postgres
      Studio URL: http://localhost:54323
    Inbucket URL: http://localhost:54324
        anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU

ローカルではキーが固定値になっています。cli のソースコードに埋め込まれています。

2.3.2 GraphQL の動作確認

動作確認は ApolloStudio を使うと簡単です

https://studio.apollographql.com/sandbox/explorer

左上の設定をクリックして

Endpoint http://localhost:54321/graphql/v1
Default headers
 apiKey eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs

を入力してください。これで特にエラーが出なければ接続成功です。左メニューのスキーマアイコンをクリックすると、デフォルトのデータ型などが確認出来ます。

2.3.3 Supabase Studio の動作確認

Studio URLに表示されているとおり http://localhost:54323 へアクセスします。

3.Todo アプリの作成

3.1 supabase の操作

3.1.1 テーブルの作成

Supabase Studio のテーブルエディターを開きます。テーブルエディタを使わず SQL で直接作ることも可能です。
以下のようにカラムを追加してテーブルを作成します

3.1.2 Postgre 上に GraphQL スキーマの作成

SQL エディタで以下のクエリを実行します

select graphql.rebuild_schema();

supabase-cli の改造版では start 時に実行するようにしています。

3.1.3 マイグレーションファイルの作成

supabase db commit create_todo

以上のコマンドで supabase/migrations にマイグレーションファイルが作成されます

3.2 Next.js でフロント部分の作成

ようやくフロント開発です。

3.2.1 必要なパッケージのインストール

yarn add @apollo/client graphql next react react-dom sass
yarn add -D @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @types/react @types/react-dom typescript

以上が今回の開発で必要なものです。

3.2.2 コードジェネレータの準備

GraphQL のスキーマーを TypeScript 用のコードに変換するための設定です

  • codegen.json
{
  "schema": {
    "http://localhost:54321/graphql/v1": {
      "headers": {
        "apiKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs"
      }
    }
  },
  "overwrite": true,
  "documents": "src/**/*.graphql",
  "generates": {
    "src/generated/graphql.tsx": {
      "plugins": [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo"
      ]
    }
  }
}

3.2.3 GraphQL 操作用の定義を作成

以下のファイルを作成し変換をかけます

  • src/graphql/todo.graphql
fragment todo on Todo {
  __typename
  id
  title
  description
  created_at
}
mutation insertTodo($value: TodoInsertInput!) {
  insertIntoTodoCollection(objects: [$value]) {
    records {
      ...todo
    }
  }
}
query queryTodo {
  todoCollection {
    edges {
      node {
        ...todo
      }
    }
  }
}
mutation deleteTodo($id: BigInt) {
  deleteFromTodoCollection(filter: { id: { eq: $id } }) {
    records {
      ...todo
    }
  }
}
  • 変換
graphql-codegen --config codegen.json

これで GraphQL の操作に必要なファイルは完成です

3.2.4 Next.js のコードを記述

  • .env.local
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321/graphql/v1
NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs

フロントからアクセスするための環境変数です

  • pages/_app.tsx
import {
  ApolloClient,
  ApolloProvider,
  InMemoryCache,
  NormalizedCacheObject,
} from "@apollo/client";
import { getMarkupFromTree } from "@apollo/client/react/ssr";
import { AppContext, AppProps } from "next/app";
import React, { useMemo } from "react";
const { renderToReadableStream } = require("react-dom/server.browser");
const URI_ENDPOINT = process.env.NEXT_PUBLIC_SUPABASE_URL;
const ApiKey = process.env.NEXT_PUBLIC_SUPABASE_KEY;

const App = (
  props: AppProps & {
    cache?: NormalizedCacheObject;
    memoryCache?: InMemoryCache;
  }
) => {
  const { Component, cache, memoryCache } = props;
  const client = useMemo(
    () =>
      new ApolloClient({
        uri: URI_ENDPOINT,
        cache: memoryCache || new InMemoryCache().restore(cache || {}),
        headers: { apiKey: ApiKey! },
      }),
    []
  );
  return (
    <ApolloProvider client={client}>
      <Component />
    </ApolloProvider>
  );
};

App.getInitialProps = async ({ Component, router }: AppContext) => {
  if (typeof window !== "undefined") return {};
  const memoryCache = new InMemoryCache();
  await getMarkupFromTree({
    tree: (
      <App
        Component={Component}
        pageProps={undefined}
        router={router}
        cache={{}}
        memoryCache={memoryCache}
      />
    ),
    renderFunction: renderToReadableStream,
  }).catch(() => {});
  return { cache: memoryCache.extract() };
};
export default App;

Next.js で SSR を行うための設定です。SSR がいらない場合は getInitialProps を消すと CSR になります。apollo-client で普通 getMarkupFromTree で SSR をすると React18 がご乱心遊ばされるので、少し細工をしています。ちなみに Next.js の公式サンプルだと SSR に getStaticProps を使っていますが、Apollo のキャッシュ機構を有効に活用するためには getInitialProps を使う必要があります。

  • src/pages/index.tsx
import { TodoContainer } from "../components/TodoContainer";

const Page = () => {
  return <TodoContainer />;
};
export default Page;
  • src/components/TodoContener/TodoContainer.tsx
import { useMemo } from "react";
import { TodoList } from "../TodoList";
import {
  useDeleteTodoMutation,
  useInsertTodoMutation,
  useQueryTodoQuery,
} from "../../generated/graphql";
import styled from "./index.module.scss";

export const TodoContainer = () => {
  const { data, refetch } = useQueryTodoQuery();
  const [insertTodo] = useInsertTodoMutation();
  const [deleteTodo] = useDeleteTodoMutation();
  const todoList = useMemo(() => {
    return data?.todoCollection?.edges
      .map((v) => v.node!)
      .sort((a, b) => (a.created_at > b.created_at ? -1 : 1));
  }, [data]);

  const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
    const form = e.target as HTMLFormElement & {
      title: HTMLInputElement;
      description: HTMLFormElement;
    };
    const title = form.title.value;
    const description = form.description.value;
    insertTodo({
      variables: { value: { title, description } },
      update: () => {
        (e.target as HTMLFormElement).reset();
        refetch();
      },
    });
    e.preventDefault();
  };
  const handleDelete = (id: number) => {
    deleteTodo({
      variables: { id },
      update: () => {
        refetch();
      },
    });
  };
  return (
    <div className={styled.root}>
      <form onSubmit={handleSubmit}>
        <button>Insert</button>
        <div>
          <input className={styled.title} id="title" placeholder="Title" />
        </div>
        <textarea
          className={styled.description}
          id="description"
          placeholder="Description"
        />
      </form>
      <TodoList todoList={todoList} onDelete={handleDelete} />
    </div>
  );
};

useQueryTodoQuery などはジェネレータで自動生成されているので、データの取得や操作まで簡単にプログラムを組むことが出来ます。

4.まとめ

GraphQL の利点を生かして Firebase よりも遙かに高い開発効率が得られます。特に TypeScript では型の恩恵が大きいので、その効果は絶大です。

今回は supabase で GraphQL を使うことが主題なので、セキュリティや認証などは全く考慮していません。その辺りをきっちり実装していくといろいろな辛みは出てきそうですが、それもプログラミングの醍醐味です。

※ この記事を書いた後に、サンプルに認証機能も載せました。詳細はまた別の記事を書きます。

GitHubで編集を提案

Discussion