アプリ
Squid を Next.js と一緒に使用する場合、アプリケーションのサーバーサイドとクライアントサイドの両方で Squid client にアクセスできます。サーバー側では、初期ペイロードの一部としてデータをクエリすることが可能です。クライアント側では、クエリやミューテーション、そしてリアルタイムのデータ更新のストリーミングを実行できます。
App Router を使用すると、use client と use server ディレクティブを使用して、クライアントでレンダリングされるコンポーネントとサーバーでレンダリングされるコンポーネントを区別することができます。React Server Components (RSCs) は、レンダリング前に非同期処理(例えばデータのクエリなど)を実行できるという点でユニークです。本チュートリアルでは、まずクライアントで Squid を使用し、その後 React Server Components を使用する方法に移行します。
まず、app/layout.tsx 内で、children を SquidContextProvider でラップします。プレースホルダーを Squid の設定オプションに置き換えてください。これらの値は Squid Console または .env ファイル内にあります。.env ファイルは creating the 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>
  );
}
ユーザーのクエリ
次に、ユーザーの一覧を表示する新しいコンポーネントを作成します。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 はアウト・オブ・ザ・ボックスで組み込みのデータベースを提供します。この例では、Users クライアントコンポーネント内で useQuery フックを使用し、データベースの users コレクションに対するクエリを実行しています。このフックはクエリと、テーブルへのライブアップデートを購読するかどうかを示すブール値を受け取ります。query().dereference() を使用すると、クエリの生データが返ってきます。
Web アプリでは、「Users」という見出しは表示されますが、ユーザーの情報が表示されません!まだコレクションにユーザーを挿入する必要があります。
もし「Loading…」やエラーメッセージが表示される場合は、Squid backend を起動しているか確認してください。tutorial-backend に移動して squid start を実行します。詳細は Running the project を参照してください。
ユーザーの挿入
データベースにユーザーを挿入する関数をトリガーするボタンコンポーネントを追加します。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(...) を実行することで、ユーザーデータがアプリケーションの組み込みデータベースに保存され、ページをリロードしてもユーザー一覧が保持されます。
サーバーでのクエリ実行
ページをリロードすると、ユーザー一覧が表示される前に一瞬 Loading…* インジケーターが表示されます。これは、Users コンポーネントがクライアントコンポーネントであり、クライアント側でユーザーのクエリを実行するのに短時間かかるためです。Next.js の App Router を使用すると、このクエリを React Server Component 内で実行し、ページロード時にそのデータをサーバーからクライアントに渡すことができます。
app/page.tsx では、Squid クエリを直接実行して初期のユーザーデータを取得し、その 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 のインスタンスは、React Server Components では React Context にアクセスできないため、Home ページ内では利用できません。代わりに、Squid React SDK をインストールすると自動的にインストールされる @squidcloud/client パッケージを使用して、別のインスタンスを作成する必要があります。コードの重複を減らすために、Squid の設定オプションを取得するための共有ユーティリティを作成することを推奨します。
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 関数をインポートし、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 フックを初期値を受け取れるように更新します:
...
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>
    );
  }
  ...
}
このコードブロックで行っている変更は以下の通りです:
- usersのリストを- useQueryフックの初期データとして渡すことで、フックから返される初期の- dataが空の配列ではなくユーザー一覧になるようにします。
- Loading… の条件を、データが存在するかどうかで判定するように更新しています。デフォルトでは、初期値を useQueryに渡していても、クライアント側でデータが正常にクエリされるまでloadingの値はtrueになります。データの存在をチェックすることで、クライアント側でクエリ中でもサーバーから得られたユーザー一覧をレンダリングできます。
これらの変更により、ページをリロードしても Loading… インジケーターは表示されず、ページロード時にユーザー一覧がすぐに表示されます。
withServerQuery を使って重複を最小限に
これまで、React Server Component 内でデータをクエリし、それをクライアントコンポーネントに渡して初期クエリ値として使用する方法を学びました。しかし、クエリのロジックが Home React Server Component と Users クライアントコンポーネントの二箇所に存在することにお気づきでしょう。これは、1箇所のクエリ変更を反映する際に更新漏れを起こしやすく、保守が難しくなります。
この重複を避けるために、Squid React SDK は withServerQuery 関数を公開しています。このフックは、サーバー上でデータをクエリし、その結果をクライアントコンポーネントに渡す処理を自動で行います。
新しい withServerQuery 関数を使用するように Home ページを更新します。この関数は以下の 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 コンポーネントにはいくつかの変更が必要です:
- コンポーネントから useQueryフックを削除します。データは、ラッピングしているwithServerQuery関数によって供給されます。
- usersプロップの名前を- dataに変更します。- withServerQueryは、ラップしたクライアントコンポーネントに- dataプロップを渡します。
- プロップの型を 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… インジケーターもデータのちらつきもなく、すべてのユーザーが表示されます。また、Insert をクリックしてもユーザー一覧は動的に更新されます。
クエリの購読の動作を実験したい場合は、withServerQuery 関数内の true を false に変更してみてください。この場合、初期ロード時にはユーザーデータは表示されますが、ユーザー挿入時にリストがリアルタイムに更新されなくなります(ユーザーはページリロード後に表示されます)。これは、クライアントコンポーネントでの変更購読を行っていないためであり、実際には Squid クエリはクライアント側で実行されなくなるためです。
購読していない場合、実質的にサーバー側のみで Squid を使用していることになり、特にリアルタイムなデータ更新が不要な場合には、完全に有効なユースケースです。
サーバーでのデータ挿入
サーバーでデータをクエリするだけでなく、Squid を使用してルーターのハンドラーやサーバーアクション内でデータを挿入することもできます。ここでは、サーバーからユーザーを挿入してみましょう。
Route handlers
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 関数を以下のように更新してください:
export default function Home(...) {
  ...
  const insertUser = async () => {
    await fetch("api/insert", { method: "POST" });
  };
  ...
}
これで、Insert ボタンをクリックするとサーバーからユーザーが挿入されます!
楽観的アップデート
ボタンをクリックしてからユーザー一覧に新しく挿入されたユーザーが表示されるまで、若干の遅延があることにお気づきかもしれません。これは、Squid がクライアント側で自動的に楽観的アップデートを処理するためです。
クライアント側で直接 insertUser を実装した場合、Squid は挿入処理を楽観的に実行し、リクエストが完了する前でも新しいユーザーを即座に表示します。もし何らかの理由で挿入が失敗した場合、Squid は楽観的な挿入をロールバックします。
サーバー側から挿入を行う場合、楽観的アップデートの恩恵を受けることができません。一般的には、Squid を API ルート内で使用して挿入することも可能ですが、クライアント側から直接挿入・更新を行う方がユーザーエクスペリエンスとして優れています。
Server Actions
ルーターのハンドラーに加え、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 は form を送信することでインポートされ、呼び出すことができます。先ほど作成した insertUser アクション関数を呼び出すために、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 はサーバー側で実行されるため、楽観的アップデートはサポートされません。