supabase + GraphQL + Next.js + React18 で SSR 対応 Todo を作る
・ソースコード
・Vercel での動作確認
・カスタム版 supabase-cli
・こちらでも同じ記事を書いています
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 に追加しました
-
修正内容
https://github.com/SoraKumo001/supabase-cli.old/pull/1/files -
勝手に修正版リリース
https://github.com/SoraKumo001/supabase-cli.old/releases
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 を使うと簡単です
左上の設定をクリックして
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 を使うことが主題なので、セキュリティや認証などは全く考慮していません。その辺りをきっちり実装していくといろいろな辛みは出てきそうですが、それもプログラミングの醍醐味です。
※ この記事を書いた後に、サンプルに認証機能も載せました。詳細はまた別の記事を書きます。
Discussion