メインコンテンツまでスキップ

Next.js and Squid

偏見のないプラットフォームである Squid は、あなたのお気に入りの frontend/fullstack フレームワークと一緒に使うことができます。Next.js を使用している場合、Squid をプロジェクトに追加することで、データソースへの接続を簡素化し、セキュリティを向上させ、リアルタイムのデータ更新をサポートするなど、さまざまな機能を利用できます。さらに、Next.js 開発者をよりサポートするために、Squid と Next.js をシームレスに開発できるようにするためのいくつかの hooks を追加しました。

TL;DR

このチュートリアルでは、Squid を Next.js アプリに統合する方法を学びます。以下の内容が含まれます:

  • クライアントに対してリアルタイムで更新をクエリおよびストリーミングする
  • ページロード中に Next.js サーバーからデータをクエリする
  • クライアントとサーバーの両方からデータをミューテートする

このドキュメントは、Pages Router を使用しているか App Router を使用しているかによって若干異なるため、下記の正しいオプションを必ずお選びください。

Create a new Next.js app

  1. ルートプロジェクトディレクトリとして next-tutorial-project を作成します:
mkdir next-tutorial-project

next-tutorial-project ディレクトリに移動し、以下のいずれかのコマンドを実行して Next.js アプリを作成します:

For App Router:

cd next-tutorial-project
npx create-next-app@latest next-tutorial --ts --tailwind --eslint --app --no-src-dir

For 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

Create the Squid backend

  1. Squid Console に移動し、next-tutorial という名前の新しいアプリケーションを作成します。
Note

Squid は dev(開発用)と prod(本番用)の2種類のターゲット環境を提供しています。このチュートリアルでは開発用に設計された dev 環境を使用します。アプリケーションが機能するためには、プロジェクト全体で dev 環境を使用していることを確認してください。詳細については、Squid の environments をご覧ください。

  1. Squid Console で、アプリケーション概要ページに移動し、Backend project セクションまでスクロールします。Initialize backend をクリックし、初期化コマンドをコピーします。

  2. ルートプロジェクトディレクトリに戻ります:

cd ..
  1. コンソールからコピーしたコマンドを使用してバックエンドを初期化します。コマンドのフォーマットは以下のとおりです:
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)]

Run the project

Squid プロジェクトをローカルで実行するには、クライアントアプリとバックエンドの Squid プロジェクトの両方をローカルで実行する必要があります。

  1. next-tutorial-backend ディレクトリに移動し、squid start を使用してバックエンドを起動します:
cd next-tutorial-backend
squid start
  1. 新しいターミナルウィンドウを開き、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 プロジェクト作成時に選択したオプションを必ずお選びください:


アプリ

Squid を Next.js と一緒に使用すると、アプリケーションのサーバーサイドとクライアントサイドの両方で Squid クライアントにアクセスできます。サーバー側では、初期ペイロードの一部としてデータをクエリすることが可能です。クライアント側では、クエリ、ミューテーションの実行、そしてリアルタイムのデータ更新のストリーミングが可能になります。

App Router を使用すると、use client および use server ディレクティブを利用して、クライアントでレンダリングされるコンポーネントとサーバーでレンダリングされるコンポーネントを区別できます。React Server Components (RSCs) は、レンダリング前にデータのクエリなどの非同期処理を行えるという点でユニークです。このチュートリアルでは、まずクライアント側で Squid を使用し、その後 React Server Components の使用に進みます。

まず、app/layout.tsx において、childrenSquidContextProvider でラップします。プレースホルダーを Squid の設定オプションに置き換えてください。これらの値は Squid Console または .env ファイル内で確認できます。.env ファイルは Squid backend の作成 時に自動生成され、backend ディレクトリに配置されています。

app/layout.tsx
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>
);
}

Query for users

次に、ユーザーのリストを表示する新しいコンポーネントを作成します。app ディレクトリの下に新しい components ディレクトリを作成し、users.tsx という新しいファイルを作成します。以下のコードを新しいファイルに追加してください。

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 内のすべてのコードを次の内容に置き換えます。

app/page.tsx
import Users from '@/components/users';

export default function Home() {
return <Users />;
}

Squid は初期状態で組み込みのデータベースを提供します。この例では、Users クライアントコンポーネント内で useQuery フックを使用して、データベース内の users コレクションのクエリを実行しています。フックはクエリと真偽値を受け取り、その真偽値がテーブルのライブアップデートの購読をするかどうかを示します。query().dereference() を使用すると、クエリの生データが返されます。

Web アプリでは、“Users” の見出しは表示されますが、ユーザーは表示されません。コレクションにユーザーを挿入する必要がまだあるのです。

Note

「Loading…」やエラーメッセージが表示された場合は、Squid backend が起動しているか確認してください。tutorial-backend に移動し、squid start を実行してください。詳細は Running the project を参照してください。

Insert a user

データベースにユーザーを挿入する関数をトリガーするボタンコンポーネントを追加します。components/users.tsx に以下を追加してください。

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 のユーザーが挿入されます。データベースクエリはライブアップデートのために購読されているため、ボタンをクリックしたとたんにユーザー ID がリストに表示されます。collection.doc().insert(...) を実行することで、ユーザーデータがアプリケーションの組み込みデータベースに永続化されるため、ページをリロードしてもユーザーのリストは保持されます。

Run a query on the server

ページをリロードすると、ユーザーのリストが表示される前に一瞬 Loading… インジケーターが表示されます。これは、Users コンポーネントがクライアントコンポーネントであり、クライアント側でユーザーのクエリを実行するのに少し時間がかかるためです。Next.js App Router を使用すると、React Server Component 内でこのクエリを実行し、ページロード時にサーバーからクライアントへそのデータを渡すことができます。

app/page.tsx では、初期ユーザーデータを取得するために直接 Squid クエリを実行し、その users のリストを Users コンポーネントの初期レンダリングに渡します:

app/page.tsx
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} />;
}
Note

最初にアプリをセットアップする際、クライアント側で SquidContextProvider を使用して Squid を初期化しました。この Squid のインスタンスは React Server Components では React Context にアクセスできないため、Home ページ内では利用できません。代わりに、Squid React SDK をインストールする際に自動的に導入される @squidcloud/client パッケージを使用して、別のインスタンスを作成する必要があります。コードの重複を避けるため、共通の設定ユーティリティを作成することをお勧めします。

utils というフォルダを作成し、squid.ts というファイルを追加します。新しいファイルに以下のコードを追加してください:

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 関数をインポートし、SquidContextProvideroptions として渡します:

app/layout.tsx
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 フックを初期値を受け取るように更新します。

app/components/users.tsx
...

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点の変更が行われています:

  1. useQuery フックに users のリストを初期データとして渡します。これにより、フックから返される初期の data は空の配列ではなく、ユーザーのリストとなります。
  2. Loading… の条件を、データが存在するかどうかでチェックするように更新しました。初期値を useQuery に渡しても、クライアント側でデータが正常にクエリされるまで loading の値は true になるため、データが存在するか確認することで、クライアント側でクエリが実行中であってもサーバー側のユーザーデータをレンダリングできます。

これらの変更により、ページのリロード時に Loading… インジケーターは表示されず、ページロードと同時にユーザーのリストが表示されます。

Minimize duplication using withServerQuery

これまで、React Server Component でデータをクエリし、それをクライアントコンポーネントに渡して初期クエリの値として使用する方法を学びました。しかし、クエリのロジックが Home React Server Component と Users クライアントコンポーネントの 2 か所に存在していることに気づくでしょう。これは、1 か所でクエリを変更してもう一方の更新を忘れると、保守が困難になる可能性があります。

重複を避けるために、Squid React SDK は withServerQuery 関数を提供しています。このフックは、サーバー側でデータをクエリし、それをクライアントコンポーネントに渡す処理を行います。

新しい withServerQuery 関数を使用するように Home ページを更新しましょう。この関数は以下の 3 つの引数を取ります:

  1. クエリデータを受け取るクライアントコンポーネント。
  2. 実行するクエリ。
  3. クエリのアップデートを購読するかどうか。

この関数は Higher Order Component を生成し、それを使用して Users コンポーネントをレンダリングします。次のように 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 コンポーネントにはいくつかの変更が必要です:

  1. コンポーネントから useQuery フックを削除します。データはすでに withServerQuery でラップされたコンポーネントに供給されています。
  2. users プロパティの名前を data に変更します。withServerQuery はラップされたクライアントコンポーネントに data プロパティを渡します。
  3. プロパティの型を WithQueryProps<User> に更新します。これは { data: Array<User> } と同等のラッパーです。
  4. if (loading) {...} および if (error) {...} の条件分岐を削除します。これらはもはや必要ありません。なぜなら、コンポーネントがレンダリングされる前に必ずデータがロードされているからです。

その結果、新しい components/users.tsx コンポーネントは次のようになります:

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… インジケーターやデータのちらつきがなくなり、すべてのユーザーが表示されます。また、Insert をクリックしても、ユーザーリストが動的に更新され続けます。

Note

クエリの購読方法を試してみたい場合は、withServerQuery 関数の第 3 引数を true から false に変更してみてください。この場合、ロード時にはユーザーデータは表示されますが、ユーザーを挿入してもユーザーリストのライブアップデートは行われません(ページをリロードするとユーザーが表示されます)。これは、クライアントコンポーネントで変更の購読を行っておらず、実際に Squid クエリ自体がクライアント側で実行されなくなるためです。

変更を購読しない場合、実質的に Squid をサーバーサイドのみに使用することになり、リアルタイムのデータ更新が不要な場合は有効なユースケースです。

Insert data on the server

サーバー側でデータをクエリするだけでなく、router handler や server actions 内で Squid を使用することもできます。ここでは、サーバーからユーザーを挿入する方法を紹介します。

Route handlers

app/api/insert/route.ts に新しいルーター ハンドラを作成し、以下のコードに置き換えてください:

app/api/insert/route.ts
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 関数と非常に似ています。目的は同じく、組み込みデータベースにユーザーを作成することですが、今回はクライアント側ではなくサーバー側で実行しています。

クライアント側からこの関数を呼び出すため、insertUser 関数を以下のように更新します:

components/users.tsx
export default function Home(...) {
...

const insertUser = async () => {
await fetch("api/insert", { method: "POST" });
};
...
}

これで、「Insert」ボタンをクリックすると、サーバーからユーザーが挿入されるようになります!

Optimistic updates

ボタンをクリックしてからユーザーがリストに表示されるまでに、わずかな遅延があることに気づくかもしれません。これは、Squid がクライアント側で自動的にオプティミスティックアップデートを処理するためです。

クライアント側で insertUser を実装した場合、挿入はクライアント上で直接行われます。この場合、Squid は挿入をオプティミスティックに実行し、挿入リクエストが完了する前に新しいユーザーを即座に表示します。もし挿入が何らかの理由で失敗した場合、Squid はオプティミスティックな挿入をロールバックします。

Tip

サーバーから挿入を行う場合、オプティミスティックアップデートの恩恵は受けられません。一般的に、API ルート内で Squid を使って挿入することもできますが、ユーザー体験を向上させるためには、クライアント側から直接挿入および更新を行うのが良いでしょう。

Server actions

Router Handler に加えて、App Router は実験的な Server Actions をサポートしています。Server Actions を使用すると、サーバー上で動的に実行できる関数を記述することが可能です。

Server Actions をサポートするために、next.config.js を以下のように更新してください:

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 は form を送信することでインポートして呼び出すことができます。先程作成した insertUser アクション関数を呼び出すために、Users コンポーネントを以下のように更新してください:

components/users.tsx
"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>
);
}
Tip

Server Actions はサーバーサイドで実行されるため、オプティミスティックアップデートはサポートされません。

And that's it!

Next.js と Squid の使い方を学んだこと、おめでとうございます!両者を組み合わせることで、シンプルで効率的なウェブ開発アプローチが実現します。さあ、開発に飛び込み、ハッピーなビルディングを!