Next.js と Squid
Squid は特定の思想に縛られない(unopinionated)プラットフォームとして、お好きなフロントエンド/フルスタックフレームワークと併用できます。Next.js を使っている場合は、プロジェクトに Squid を追加することで、データソースへの接続を効率化し、追加のセキュリティを提供し、リアルタイムのデータ更新をサポートするなど、さまざまなことが可能になります。さらに Next.js 開発者をよりよくサポートするために、Squid と Next.js を組み合わせた開発をシームレスにするいくつかの hooks も追加しました。
TL;DR
このチュートリアルでは、Squid を Next.js アプリに統合する方法を学びます。内容は次のとおりです。
- クライアントに対してリアルタイムでクエリと更新のストリーミングを行う
- ページロード時に Next.js サーバーからデータをクエリする
- クライアントとサーバーの両方からデータを変更(mutate)する
このドキュメントは、Pages Router を使っているか App Router を使っているかによって少し異なるため、下の正しいオプションを選択してください。
新しい Next.js アプリを作成する
next-tutorial-projectというルートプロジェクトディレクトリを作成します。
mkdir next-tutorial-project
next-tutorial-project ディレクトリに移動し、次のいずれかのコマンドを実行して Next.js アプリを作成します。
App Router の場合:
cd next-tutorial-project
npx create-next-app@latest next-tutorial --ts --tailwind --eslint --app --no-src-dir
Pages Router の場合:
cd next-tutorial-project
npx create-next-app@latest next-tutorial --ts --tailwind --eslint --no-src-dir
このプロジェクトについて、いくつか注意点があります。
- このプロジェクトは TypeScript を使用していますが、ご自身のプロジェクトでは JavaScript を使用しても構いません。
- Pages Router と App Router のどちらも Squid と統合できます。このチュートリアルでは好みの方法を選んでください。
- このプロジェクトは Tailwind CSS でスタイリングされています。
プロジェクトを作成したら、next-tutorial ディレクトリに移動して Squid React SDK をインストールします。Squid React SDK は、Squid を React および Next.js プロジェクトに統合するための hooks とユーティリティを提供します。
cd next-tutorial
npm install @squidcloud/react
Squid backend を作成する
- Squid Console に移動し、
next-tutorialという名前の新しい application を作成します。
Squid には 2 つの target environment があります。開発用の dev と、本番用の prod です。このチュートリアルでは開発向けに設計された dev environment を使用します。アプリケーションが動作するように、プロジェクト全体を通して dev environment を使用していることを確認してください。詳しくは Squid の environments を参照してください。
-
Squid Console で application の overview ページに移動し、Backend project セクションまでスクロールします。Initialize backend をクリックし、初期化コマンドをコピーします。
-
ルートプロジェクトディレクトリに移動します。
cd ..
- Console からコピーしたコマンドを使って backend を初期化します。コマンドの形式は次のとおりです。
squid init next-tutorial-backend --appId [YOUR_APP_ID] --apiKey [YOUR_API_KEY] --environmentId dev --squidDeveloperId [YOUR_SQUID_DEVELOPER_ID] --region [YOUR_REGION (likely us-east-1.aws)]
プロジェクトを実行する
Squid プロジェクトをローカルで実行するには、クライアントアプリと backend の Squid プロジェクトの両方をローカルで実行する必要があります。
next-tutorial-backendディレクトリに移動し、squid startを使って backend を起動します。
cd next-tutorial-backend
squid start
- 新しいターミナルウィンドウを開き、
next-tutorialディレクトリに移動します。その後、アプリを実行します。
cd next-tutorial
npm run dev
これで Next.js アプリプロジェクトは http://localhost:PORT で稼働しています。PORT はターミナルに表示されます。まだページに表示する内容を編集していないため、表示されるのは Next.js のスタータープロジェクトです。
Router
ここから先は、Next.js を App Router で使っているか Pages Router で使っているかによって、このチュートリアルの内容が分岐します。Next.js プロジェクト作成時に選択したオプションを選んでください。
- App Router
- Pages Router
アプリ
Next.js で Squid を使用する場合、アプリケーションのサーバー側とクライアント側の両方で Squid client にアクセスできます。サーバーでは、初期ペイロードの一部としてデータをクエリできます。クライアントでは、query、mutation を実行したり、リアルタイムのデータ更新をストリーミングで受け取ったりできます。
App Router では、use client と use server ディレクティブを使って、クライアントでレンダリングされるコンポーネントとサーバーでレンダリングされるコンポーネントを区別できます。React Server Components (RSCs) は、レンダリング前に(データのクエリのような)非同期処理を実行できる点が特徴です。このチュートリアルでは、まずクライアントで Squid を使い始め、その後 React Server Components を使う流れに進みます。
まず app/layout.tsx で、children を SquidContextProvider でラップします。プレースホルダーを Squid の設定オプションに置き換えてください。これらの値は Squid Console または .env ファイルで確認できます。.env ファイルは Squid backend の作成 時に自動生成され、backend ディレクトリ内にあります。
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { SquidContextProvider } from '@squidcloud/react';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<SquidContextProvider
options={{
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
}}
>
{children}
</SquidContextProvider>
</body>
</html>
);
}
users をクエリする
次に、ユーザー一覧を表示する新しいコンポーネントを作成します。app ディレクトリ配下に新しい components ディレクトリと、users.tsx という新しいファイルを作成してください。新しいファイルに以下のコードを追加します。
'use client';
import { useCollection, useQuery } from '@squidcloud/react';
type User = {
id: string;
};
export default function Users() {
const collection = useCollection<User>('users');
const { loading, data, error } = useQuery(collection.query().dereference());
if (loading) {
return <div className="flex flex-col items-center justify-center min-h-screen">Loading...</div>;
}
if (error) {
return <div className="flex flex-col items-center justify-center min-h-screen">{error.message}</div>;
}
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<span>Users</span>
<ul>
{data.map((user) => (
<li key={user.id}>{user.id}</li>
))}
</ul>
</div>
);
}
Users コンポーネントをレンダリングするために、app/page.tsx のコードをすべて次の内容に置き換えます。
import Users from '@/components/users';
export default function Home() {
return <Users />;
}
Squid は標準で組み込み database を提供します。この例では、Users クライアントコンポーネント内で useQuery hook を使い、database の users collection に対するクエリを実行します。hook はクエリと boolean を受け取り、その boolean はテーブルのライブ更新に subscribe するかどうかを示します。query().dereference() を使うと、クエリの raw data が返ります。
Web アプリでは “Users” の見出しが表示されますが、ユーザーがいません! まだ collection にユーザーを insert していないためです。
“Loading…” や error message が表示される場合は、Squid backend を起動したか確認してください。tutorial-backend に移動して squid start を実行します。Running the project を参照してください。
ユーザーを insert する
database にユーザーを insert する関数をトリガーする button コンポーネントを追加します。components/users.tsx に以下を追加してください。
import { useCollection, useQuery } from "@squidcloud/react";
...
export default function Users() {
...
const insertUser = async () => {
await collection.doc().insert({
id: crypto.randomUUID(),
});
}
...
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<button onClick={insertUser}>Insert</button>
<span>Users</span>
<ul>
{data.map((user) => (
<li key={user.id}>{user.id}</li>
))}
</ul>
</div>
);
Web アプリに Insert ボタンが表示されます。ボタンをクリックするとランダムな ID を持つユーザーが insert されます。database クエリはライブ更新に subscribe されているため、ボタンをクリックした直後にユーザー ID がユーザー一覧に表示されます。collection.doc().insert(...) を実行すると、ユーザーデータがアプリケーションの組み込み database に永続化されるため、ページをリフレッシュしてもユーザー一覧は保持されます。
サーバーでクエリを実行する
ページをリフレッシュすると、ユーザー一覧が表示される前に Loading…* のインジケーターが短時間表示されます。これは Users コンポーネントがクライアントコンポーネントであり、クライアント側で users をクエリするのに少し時間がかかるためです。Next.js App Router では、このクエリを React Server Component 内で実行し、ページロード時にそのデータをサーバーからクライアントへ渡せます。
app/page.tsx で Squid query を直接実行して初期 user data を取得し、その users のリストを Users コンポーネントの初期レンダリングに渡します。
import Users from '@/components/users';
import { Squid } from '@squidcloud/client';
type User = {
id: string;
};
export default async function Home() {
const squid = Squid.getInstance({
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
});
const users = await squid.collection<User>('users').query().dereference().snapshot();
return <Users users={users} />;
}
アプリの初期設定では、クライアント側で SquidContextProvider により Squid を初期化しました。この Squid インスタンスは Home ページ内からはアクセスできません。React Server Components は React Context にアクセスできないためです。代わりに、Squid React SDK をインストールすると自動的にインストールされる @squidcloud/client package を使って、別のインスタンスを作成する必要があります。コードの重複を減らすために、Squid options を取得する共有 utility を作ることを推奨します。
utils というフォルダを作成し、squid.ts というファイルを追加します。新しいファイルに以下のコードを追加してください。
import { SquidOptions } from '@squidcloud/client';
export function getOptions(): SquidOptions {
return {
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
};
}
これで options={...} はどこでも options={getOptions()} に置き換えられます。
app/layout.tsx で getOptions 関数を import し、SquidContextProvider の options として渡します。
import { getOptions } from "@/utils/squid";
...
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<SquidContextProvider options={getOptions()}>
{children}
</SquidContextProvider>
</body>
</html>
);
}
サーバーでクエリした users にアクセスできるようになったので、Users コンポーネントの useQuery hook を更新して初期値を受け取れるようにします。
...
export default function Users({ users }: { user: Array<User> }) {
...
const { loading, data, error } = useQuery(
collection.query().dereference(),
{ initialData: users }
);
...
if (loading && !data.length) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
Loading...
</div>
);
}
...
}
このコードブロックでは 2 つの変更を行っています。
useQueryhook に初期データとしてusersのリストを渡します。これにより hook から返される初期dataが空配列ではなくユーザー一覧になります。- Loading… の条件を「データが存在するか」を確認するように更新します。デフォルトでは、
useQueryに初期値を渡しても、クライアントでデータのクエリが成功するまではloadingはtrueのままです。dataの存在をチェックすることで、クライアント側のクエリがまだ loading 中でも、サーバーからのユーザー一覧をレンダリングできます。
これらの変更により、ページをリフレッシュしても Loading… インジケーターは表示されなくなります。代わりに、ページロード直後からユーザー一覧が表示されます。
withServerQuery を使って重複を最小化する
ここまでで、React Server Component でデータをクエリし、それをクライアントコンポーネントに渡して初期クエリ値として使う方法を学びました。しかし、クエリのロジックが 2 箇所(Home の React Server Component と Users のクライアントコンポーネント)に存在していることに気づくでしょう。これは、片方のクエリを変更してもう片方を更新し忘れるなど、保守が難しくなる場合があります。
重複を避けるために、Squid React SDK には withServerQuery 関数が用意されています。この hook は、サーバー側でデータをクエリし、それをクライアントコンポーネントへ渡す処理を担います。
Home ページを更新して新しい withServerQuery 関数を使用します。この関数は 3 つの引数を取ります。
- クエリデータを受け取るクライアントコンポーネント
- 実行するクエリ
- クエリ更新に subscribe するかどうか
この関数は、Users コンポーネントをレンダリングするために使える Higher Order Component を生成します。app/page.tsx を更新して新しい関数を含めてください。
import Users from '@/components/users';
import { Squid } from '@squidcloud/client';
import { getOptions } from '@/utils/squid';
import { withServerQuery } from '@squidcloud/react';
type User = {
id: string;
};
export default async function Home() {
const squid = Squid.getInstance(getOptions());
const UsersWithQuery = withServerQuery(
Users,
squid.collection<User>('users').query().dereference(),
true
);
return <UsersWithQuery />;
}
この新しい関数に合わせて、Users コンポーネント側にもいくつか変更が必要です。
- コンポーネントから
useQueryhook を削除します。データはwithServerQueryのラッパー関数から供給されるようになります。 usersprop をdataにリネームします。withServerQueryはラップしたクライアントコンポーネントにdataprop を渡します。- prop の型を
WithQueryProps<User>に更新します。これは実質的に{ data: Array<User> }を表すラッパーです。 if (loading) {...}とif (error) {...}の条件分岐を削除します。コンポーネントがレンダリングされる前にデータがロードされるため、不要になります。
その結果、新しい components/users.tsx は次のようになります。
'use client';
import { useCollection, WithQueryProps } from '@squidcloud/react';
type User = {
id: string;
};
export default function Users({ data }: WithQueryProps<User>) {
const collection = useCollection<User>('users');
const insertUser = async () => {
await collection.doc().insert({
id: crypto.randomUUID(),
});
};
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<button onClick={insertUser}>Insert</button>
<span>Users</span>
<ul>
{data.map((user) => (
<li key={user.id}>{user.id}</li>
))}
</ul>
</div>
);
}
Next.js アプリをリロードしてください。Loading… インジケーターもデータのちらつきもなく、すべての users が表示されます。さらに Insert をクリックすると、ユーザー一覧が引き続き動的に更新されます。
クエリの subscribe の動作を試したい場合は、withServerQuery 関数内の true を false に変更してみてください。この場合でもロード時にはユーザーデータが表示されますが、ユーザーを insert してもユーザー一覧はライブ更新されません(ページをリロードするとユーザーが表示されます)。これは、クライアントコンポーネント内で変更に subscribe しなくなるためであり、実際 Squid query はクライアントではまったく実行されなくなります。
変更に subscribe しない場合、実質的にサーバー側だけで Squid を使っていることになります。これは完全に有効なユースケースで、特にリアルタイムのデータ更新が不要な場合に適しています。
サーバーでデータを insert する
サーバーでのクエリに加えて、router handlers や server actions 内でも Squid を使用できます。サーバーからユーザーを insert してみましょう。
Route handlers
app/api/insert/route.ts 配下に新しい router handler を作成し、この route を次のコードに置き換えます。
import { getOptions } from '@/utils/squid';
import { Squid } from '@squidcloud/client';
import { NextResponse } from 'next/server';
type User = {
id: string;
};
export async function POST() {
const squid = Squid.getInstance(getOptions());
const user = {
id: crypto.randomUUID(),
};
await squid.collection<User>('users').doc().insert(user);
return NextResponse.json(user);
}
このコードが Users コンポーネント内の insertUser 関数とよく似ていることに気づくでしょう。どちらも本質的には同じ目的(組み込み database にユーザーを作成する)を果たしますが、今度はクライアントではなくサーバーで実行されます。
クライアントからこの関数を呼び出すために、insertUser 関数を次のように更新します。
export default function Home(...) {
...
const insertUser = async () => {
await fetch("api/insert", { method: "POST" });
};
...
}
“Insert” ボタンをクリックすると、サーバーからユーザーが insert されるようになります。
Optimistic updates
ボタンをクリックしてから、insert されたユーザーがユーザー一覧に表示されるまでに少し遅延があることに気づくでしょう。これは、Squid がクライアントで optimistic updates を自動的に扱う仕組みによるものです。
insertUser のクライアント側実装では、insert がクライアントで直接行われます。この場合 Squid は optimistic に insert を行うため、insert リクエストがまだ処理中であっても新しいユーザーが即座に表示されます。そして、何らかの理由で insert が失敗した場合、Squid は optimistic insert を rollback します。
サーバーから insert する場合、optimistic updates の恩恵は失われます。一般に、Squid を API routes 内で使って insert することもできますが、ユーザー体験としてはクライアントから直接 insert と update を行う方が良いケースが多いです。
Server actions
Route Handler に加えて、App Router では実験的な Server Actions をサポートしています。Server Actions を使うと、サーバー上で動的に実行できる関数をユーザーが書けます。
Server Actions を有効にするため、next.config.js を更新します。
module.exports = {
experimental: {
serverActions: true,
},
};
actions という新しいフォルダを作成し、insert.tsx というファイルを追加して、次のコードを書きます。use server ディレクティブは、このファイルが Server Action を表すことを示します。
'use server';
import { Squid } from '@squidcloud/client';
import { getOptions } from '@/utils/squid';
type User = {
id: string;
};
export default async function insertUser() {
const squid = Squid.getInstance(getOptions());
await squid.collection<User>('users').doc().insert({
id: crypto.randomUUID(),
});
}
クライアントでは、Server Actions は import して form を submit することで呼び出せます。先ほど作成した insertUser action を呼び出すには、Users コンポーネントを次のように更新します。
"use client";
import insertAction from '@/actions/insert';
...
export default function Users({ data }: WithQueryProps<User>) {
...
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<form action={insertAction}>
<button type="submit">Insert</button>
</form>
<span>Users</span>
<ul>
{data.map((user) => (
<li key={user.id}>{user.id}</li>
))}
</ul>
</div>
);
}
Server Actions はサーバー側で実行されるため、optimistic updates はサポートされません。
ページ
Next.js で Squid を使用する場合、アプリケーションのサーバー側とクライアント側の両方で Squid client にアクセスできます。サーバーでは、初期ペイロードの一部としてデータをクエリできます。クライアントでは、クエリやミューテーション(mutation)を実行したり、リアルタイムのデータ更新をストリーミングで受け取ったりできます。
まず pages/_app.tsx で、Component を SquidContextProvider でラップします。プレースホルダーを Squid の設定オプションに置き換えてください。これらの値は Squid Console または .env ファイルで確認できます。.env ファイルは Squid backend の作成 時に自動生成され、backend ディレクトリに配置されています。
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import { SquidContextProvider } from '@squidcloud/react';
export default function App({ Component, pageProps }: AppProps) {
return (
<SquidContextProvider
options={{
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
}}
>
<Component {...pageProps} />
</SquidContextProvider>
);
}
users のクエリ
アプリに users コレクションへのクライアントサイドクエリを導入するには、pages/index.tsx を次のコードに置き換えます。
import { useCollection, useQuery } from '@squidcloud/react';
type User = {
id: string;
};
export default function Home() {
const collection = useCollection<User>('users');
const { loading, data, error } = useQuery(collection.query().dereference());
if (loading) {
return <div className="flex flex-col items-center justify-center min-h-screen">Loading...</div>;
}
if (error) {
return <div className="flex flex-col items-center justify-center min-h-screen">{error.message}</div>;
}
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<span>Users</span>
<ul>
{data.map((user) => (
<li key={user.id}>{user.id}</li>
))}
</ul>
</div>
);
}
Squid には標準で組み込みデータベースが用意されています。この例では、Users クライアントコンポーネントで useQuery フックを使い、データベースの users コレクションをクエリします。このフックはクエリと boolean を受け取り、テーブルのライブアップデートに subscribe(購読)するかどうかを示します。query().dereference() を使用すると、クエリの生データが返されます。
Web アプリでは “Users” という見出しは表示されますが、ユーザーがいません! まだコレクションにユーザーを挿入する必要があります。
“Loading…” やエラーメッセージが表示される場合は、Squid backend を起動したか確認してください。tutorial-backend に移動して squid start を実行します。Running the project を参照してください。
ユーザーの挿入
データベースにユーザーを挿入する関数をトリガーする button コンポーネントを追加します。pages/index.tsx に次を追加してください。
import { useCollection, useQuery } from "@squidcloud/react";
...
export default function Home() {
...
const insertUser = async () => {
await collection.doc().insert({
id: crypto.randomUUID(),
});
}
...
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<button onClick={insertUser}>Insert</button>
<span>Users</span>
<ul>
{data.map((user) => (
<li key={user.id}>{user.id}</li>
))}
</ul>
</div>
);
Web アプリに Insert ボタンが表示されます。ボタンをクリックすると、ランダムな ID を持つユーザーが挿入されます。データベースクエリはライブアップデートに subscribe されているため、ボタンをクリックするとすぐにユーザー ID がユーザー一覧に表示されます。collection.doc().insert(...) を実行すると、ユーザーデータがアプリケーションの組み込みデータベースに永続化されるので、ページをリフレッシュしてもユーザー一覧は保持されます。
サーバーでクエリを実行する
ページをリフレッシュすると、ユーザー一覧が表示される前に Loading… インジケーターが短時間表示されます。これは Users コンポーネントがクライアントコンポーネントであり、クライアント側でユーザーをクエリするのに少し時間がかかるためです。Next.js App Router では、このクエリを React Server Component 内で実行し、ページロード時にサーバーからクライアントへそのデータを渡せます。
pages/index.tsx に新しい static getServerSideProps 関数を作成します。この関数は初期ユーザーデータを Squid にクエリし、その users のリストを Home コンポーネントの初期レンダリングへ渡します。
import { useCollection, useQuery } from "@squidcloud/react";
import { Squid } from "@squidcloud/client";
...
export const getServerSideProps = (async () => {
const squid = Squid.getInstance({
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
});
const users = await squid
.collection<User>("users")
.query()
.dereference()
.snapshot();
return { props: { users } };
}) satisfies GetServerSideProps<{
users: Array<User>;
}>;
export default function Home({
users,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
...
}
最初にアプリをセットアップしたとき、クライアントでは SquidContextProvider で Squid を初期化しました。この Squid のインスタンスは Home ページ内からはアクセスできません。React Server Components は React Context にアクセスできないためです。代わりに、@squidcloud/client パッケージを使って別のインスタンスを作成する必要があります(Squid React SDK のインストール時に自動的にインストールされます)。コードの重複を減らすため、Squid options を取得する共有ユーティリティを作成することをおすすめします。
utils というフォルダを作成し、squid.ts というファイルを追加します。新しいファイルに次のコードを追加してください。
import { SquidOptions } from '@squidcloud/client';
export function getOptions(): SquidOptions {
return {
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
};
}
これで、options={...} を options={getOptions()} に置き換えられます。
pages/_app.tsx で getOptions 関数を import し、SquidContextProvider の options として渡します。
import { getOptions } from "@/utils/squid";
...
export default function App({ Component, pageProps }: AppProps) {
return (
<SquidContextProvider options={getOptions()}>
<Component {...pageProps} />
</SquidContextProvider>
);
サーバーでクエリした users にアクセスできるようになったので、useQuery フックが初期値を受け取れるように更新できます。
...
export default function Home({
users,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
...
const { loading, data, error } = useQuery(
collection.query().dereference(),
{ initialData: users }
);
...
if (loading && !data.length) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
Loading...
</div>
);
}
...
}
このコードブロックでは 2 つの変更を行っています。
usersのリストを初期データとしてuseQueryフックに渡します。これにより、フックから返される初期のdataが空配列ではなくユーザー一覧になります。- Loading… の条件を、データが存在するかどうかを確認するように更新します。デフォルトでは、
useQueryに初期値を渡しても、クライアント側でのクエリが成功するまでloadingはtrueのままです。dataの存在をチェックすることで、クライアント側のクエリがまだ loading 中でも、サーバーから渡されたユーザー一覧をレンダリングできます。
これらの変更により、ページをリフレッシュしても Loading… インジケーターは表示されません。代わりに、ページロード直後からユーザー一覧が表示されます。
サーバーでデータを扱う
getServerSideProps 内で Squid を使うことに加えて、API routes でも Squid を使えます! クライアントからではなく、API route からユーザーを挿入してみましょう。
- デフォルトで Next.js は
pages/api/hello.tsファイルを生成します。このファイル名をinsert.tsに変更します。 pages/api/insert.tsを次のコードに置き換えます。
import type { NextApiRequest, NextApiResponse } from 'next';
import { getOptions } from '@/utils/squid';
import { Squid } from '@squidcloud/client';
type User = {
id: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<User>) {
const squid = Squid.getInstance(getOptions());
const user = {
id: crypto.randomUUID(),
};
await squid.collection<User>('users').doc().insert(user);
res.status(200).json(user);
}
このコードが Home コンポーネント内の insertUser 関数と非常によく似ていることに注目してください。目的は同じ(組み込みデータベースにユーザーを作成する)ですが、クライアントではなくサーバーで実行されます。
クライアントからこの関数を呼び出すには、insertUser 関数を次のように更新します。
export default function Home(...) {
...
const insertUser = async () => {
await fetch("api/insert", { method: "POST" });
};
...
}
“Insert” ボタンをクリックしてユーザーを挿入してください。今度はサーバーが insert を処理します。
Optimistic updates
ボタンをクリックしてから、新しく挿入されたユーザーがユーザー一覧に表示されるまでに小さな遅延があることに気づくはずです。これは、Squid がクライアントで optimistic updates を自動的に扱う仕組みによるものです。
クライアントサイド実装の insertUser では、insert はクライアントで直接実行されます。この場合、Squid は optimistic に insert を行うため、insert リクエストがまだ処理中であっても新しいユーザーが即座に表示されます。何らかの理由で insert が失敗した場合、Squid は optimistic insert をロールバックします。
サーバーから insert すると、optimistic updates の恩恵を失います。一般に、API routes 内で Squid を使って insert することも可能ですが、多くの場合、クライアントから直接 insert / update したほうがユーザー体験は良くなります。
以上です!
Next.js と Squid の使い方を学習できました。両者を組み合わせることで、シンプルで効率的な Web 開発のアプローチが得られます。ぜひ取り組んで、楽しく開発してください!