Next.js と Squid
unopinionated platform である Squid は、お好みの frontend/fullstack framework と併用できます。Next.js を使用している場合は、Squid を project に追加して、data source への connection を streamline し、additional security を追加し、real-time data update をサポートするなど、多くのことを実現できます。さらに、Next.js developer をより適切に support するために、Squid と Next.js による development を seamless な experience にする hook をいくつか追加しています。
TL;DR
この tutorial では、Squid を Next.js app に integrate する方法を学びます。以下を含みます。
- client への real-time query と update streaming
- page load 中に Next.js server から data を query
- client と server の両方から data を mutate
この documentation は、Pages Router と App Router のどちらを使用しているかによって少し異なるため、以下で正しい option を選択してください。
新しい Next.js App を作成する
next-tutorial-projectという root project directory を作成します。
mkdir next-tutorial-project
next-tutorial-project directory に移動し、以下のいずれかの command を実行して Next.js app を作成します。
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
project について注意すべき点:
- project は Typescript を使用しますが、独自 project では JavaScript を使用できます。
- Pages Router と App Router はどちらも Squid と integrate できます。この tutorial では希望する method を選択してください。
- project は Tailwind CSS で style されています。
project の作成後、next-tutorial directory に移動し、Squid React SDKを install します。Squid React SDK は、Squid を React および Next.js project に integrate するための hook と utility を提供します。
cd next-tutorial
npm install @squidcloud/react
Squid Backend を作成する
- Squid Console に移動し、
next-tutorialという名前の新しい application を作成します。
Squid は 2 つの異なる target environment を提供します。development 用の dev と production 用の prod です。この tutorial は development 向けに設計されているため、dev environment を使用します。application を機能させるには、project 全体で dev environment を使用していることを確認してください。詳細については、Squid の environmentを参照してください。
-
Squid Console で application overview page に移動し、Backend project section まで scroll します。Initialize backend をクリックして initialization command を copy します。
-
root project directory に移動します。
cd ..
- console から copy した command を使用して backend を初期化します。command の format は以下のとおりです。
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)]
Project を実行する
Squid project をローカルで実行するには、client app と backend Squid project の両方をローカルで実行する必要があります。
next-tutorial-backenddirectory に移動し、squid startを使用して backend を開始します。
cd next-tutorial-backend
squid start
- 新しい terminal window を開いて
next-tutorialdirectory に移動し、app を実行します。
cd next-tutorial
npm run dev
これで Next.js app project は、terminal に log 出力された PORT の http://localhost:PORT で実行されます。まだ page に render される内容を edit していないため、表示される app は Next.js starter project です。
Router
ここから、この tutorial は App Router または Pages Router を使用する Next.js によって分岐します。Next.js project の作成時に選択した option を選んでください。
- App Router
- Pages Router
App
Next.js で Squid を使用する場合、application の server side と client side の両方で Squid client に access できます。server では、initial payload の一部として data に query を実行できます。client では、query、mutation、real-time data update の stream を実行できます。
App Router では、use client と use server directive を使用して、client で render される component と server で render される component を区別できます。React Server Components(RSC)は、render 前に非同期処理(data の query など)を実行できる点で unique です。この tutorial では、client で Squid を使用することから始め、React Server Components での使用へ進みます。
最初に、app/layout.tsx で children を SquidContextProvider で wrap します。placeholder を Squid configuration option に置き換えます。これらの value は Squid Console または .env file にあります。.env file は Squid backend の作成時に自動生成され、backend directory にあります。
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>
);
}
User に Query を実行する
次に、user list を表示する新しい component を作成します。app directory の下に、新しい components directory と users.tsx という新しい file を作成します。新しい file に以下の code を追加します。
'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 component を render するには、app/page.tsx のすべての code を以下に置き換えます。
import Users from '@/components/users';
export default function Home() {
return <Users />;
}
Squid は out of the box で built-in database を提供します。この example では、Users client component の useQuery hook を使用して、database の users collection に query を実行します。hook は query と boolean を受け取ります。boolean は table の live update を subscribe するかを示します。query().dereference() を使用すると、query の raw data が返されます。
web app に “Users” heading が表示されますが、user はまだいません。collection に user を insert する必要があります。
“Loading…” または error message が表示される場合は、Squid backend を開始したことを確認してください。tutorial-backend に移動して squid start を実行します。Project の実行を参照してください。
User を Insert する
database に user を insert する function を trigger する button component を追加します。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 app に Insert button が表示されます。button をクリックすると random ID の user が insert されます。database query は live update を subscribe しているため、button をクリックするとすぐに user ID が user list に表示されます。collection.doc().insert(...) の実行により user data は application の built-in database に persist されるため、page を refresh しても user list は保持されます。
Server で Query を実行する
page を refresh すると、user list が表示される前に Loading…* indicator が一時的に表示されます。これは Users component が client component であり、client で user に query を実行するのに短い時間がかかるためです。Next.js App Router では、この query を React Server Component 内で実行し、page load 中に server から client に data を渡せます。
app/page.tsx で Squid query を直接実行して initial user data を取得し、users list を Users component の initial rendering に渡します。
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} />;
}
app を最初に setup した際、SquidContextProvider で client 上の Squid を初期化しました。React Server Component は React Context に access できないため、この Squid instance は Home page 内では access できません。代わりに、Squid React SDK の install 時に自動 install される @squidcloud/client package を使用して、別の instance を作成する必要があります。code の repetition を減らすため、Squid option を取得する shared utility の作成をおすすめします。
utils という folder を作成し、squid.ts という file を追加します。新しい file に以下の code を追加します。
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 function を 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>
);
}
server で query した users に access できたので、Users component の useQuery hook を update して initial value を受け取れるようにします。
...
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>
);
}
...
}
この code block は 2 つの change を行います。
userslist を initial data としてuseQueryhook に渡します。これにより hook が返す initialdataは empty array ではなく user list になります。- Loading… condition を update し、data が存在するかを check します。default では、
useQueryに initial value を渡した場合でも、client で data の query が成功するまでloadingvalue はtrueになります。dataの存在を check すると、client で query の loading 中にも server の user list を render できます。
これらの change により、page の refresh 時に Loading… indicator は表示されなくなります。代わりに page の load 時点で user list が表示されます。
withServerQuery を使用して Duplication を最小化する
ここまでで、React Server Component で data に query を実行し、それを client component に渡して initial query value として使用する方法を学びました。ただし、query logic が 2 か所、Home React Server Component と Users client component に存在していることに気付くでしょう。特に一方の location の query を変更して、もう一方の update を忘れると、maintenance が難しくなります。
duplication を避けるため、Squid React SDK は withServerQuery function を公開しています。この hook は server での data query と client component への data の渡しを処理します。
新しい withServerQuery function を使用するように Home page を update します。この function は 3 つの argument を取ります。
- query data を受け取る client component。
- 実行する query。
- query update を subscribe するかどうか。
この function は次に、Users component の render に使用できる Higher Order Component を生成します。新しい function を含めるよう app/page.tsx を update します。
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 />;
}
この新しい function を使用するには、Users component にいくつかの change が必要です。
- component から
useQueryhook を remove します。data は wrappingwithServerQueryfunction から提供されます。 usersprop の name をdataに変更します。withServerQueryは wrap する client component にdataprop を渡します。- prop type を
WithQueryProps<User>に update します。これは実質的に{ data: Array<User> }へ translate される wrapper です。 if (loading) {...}とif (error) {...}conditional を remove します。component の render 前に data が load されるため、これらは不要です。
その結果、新しい components/users.tsx component は次のようになります。
'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 app を reload します。これで Loading… indicator や data の flicker なしで、すべての user を確認できます。さらに、Insert をクリックしても user list は dynamic に update されます。
query の subscription の動作を experiment するには、withServerQuery function 内の true を false に変更してみてください。この場合も load 時には user data が表示されますが、user の insert による user list の live update は発生しません(page の reload 後には user が表示されます)。これは、client component で change を subscribe しなくなり、実際には Squid query が client でまったく実行されなくなるためです。
change を subscribe しない場合、実質的に Squid を server side のみで使用します。これは、特に real-time data update が不要な場合には完全に valid な use case です。
Server に Data を Insert する
server で data に query を実行するだけでなく、router handler と server action 内でも Squid を使用できます。server から user を insert してみましょう。
Route Handler
app/api/insert/route.ts の下に新しい router handler を作成し、この route を以下の code に置き換えます。
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);
}
この code は Users component の insertUser function と非常によく似ています。目的は同じで、built-in database に user を作成しますが、今回は client ではなく server で実行されます。
client からこの function を call するには、insertUser function を以下に update します。
export default function Home(...) {
...
const insertUser = async () => {
await fetch("api/insert", { method: "POST" });
};
...
}
“Insert” button をクリックすると、server から user が insert されます!
Optimistic Update
button をクリックしてから、新しく insert された user が user list に表示されるまでに小さな delay があることに気付くでしょう。これは Squid が client で optimistic update を自動処理する方法によるものです。
client-side implementation の insertUser では、insert は client で直接行われます。この場合、Squid は optimistically に insert を実行するため、insert request が in flight 中でも新しい user が即座に表示されます。何らかの理由で insert が failure した場合、Squid は optimistic insert を rollback します。
server から insert する場合、optimistic update の benefit は失われます。一般に、API route 内で Squid を使用して insert はできますが、client から直接 insert・update する方が user experience が優れていることが多いです。
Server Action
Router Handler に加え、App Router の使用時は experimental Server Actions をサポートします。Server Actions を使用すると、server で dynamic に実行できる function を記述できます。
Server Actions をサポートするには、next.config.js を update します。
module.exports = {
experimental: {
serverActions: true,
},
};
actions という新しい folder を作成し、以下の code を持つ insert.tsx file を追加します。use server directive は、この file が 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(),
});
}
client では、form の submit により Server Action を import・call できます。作成した action insertUser function を call するには、以下のように Users component を update します。
"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 Action は server-side で実行されるため、optimistic update をサポートしません。
Pages
Next.js で Squid を使用する場合、application の server side と client side の両方で Squid client に access できます。server では initial payload の一部として data を query できます。client では query、mutation、real-time data update の stream を実行できます。
まず、pages/_app.tsx で Component を SquidContextProvider で wrap します。placeholder を Squid configuration option に置き換えます。これらの value は Squid Console または .env file で確認できます。.env file は Squid backend の作成時に自動生成され、backend directory にあります。
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>
);
}
User を Query する
app に users collection の client-side query を導入するには、pages/index.tsx を以下の code に置き換えます。
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 はすぐに使用できる built-in database を提供します。この example では Users client component の useQuery hook を使用し、database の users collection に query を実行します。hook は query と boolean を受け取ります。boolean は table の live update を subscribe するかを示します。query().dereference() を使用すると query の raw data が返されます。
web app には “Users” heading が表示されますが、user はいません。collection に user を insert する必要があります。
“Loading…” または error message が表示される場合は、Squid backend を開始していることを確認してください。tutorial-backend に移動して squid start を実行します。Project の実行を参照してください。
User を Insert する
database に user を insert する function を trigger する button component を追加します。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 app に Insert button が表示されます。button をクリックすると random ID を持つ user が insert されます。database query は live update を subscribe しているため、button をクリックすると user ID がすぐに user list に表示されます。collection.doc().insert(...) を実行すると user data が application の built-in database に persist されるため、page を refresh しても user list は保持されます。
Server で Query を実行する
page を refresh すると、user list が表示される前に Loading… indicator が短時間表示されます。これは Users component が client component であり、client で user を query するのに少し時間がかかるためです。Next.js App Router では、この query を React Server Component 内で実行し、page load 時に server から client に data を渡せます。
pages/index.tsx で新しい static getServerSideProps function を作成します。この function は initial user data を Squid で query し、users list を Home component の initial rendering に渡します。
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>) {
...
}
app の initial setup 時には、client で SquidContextProvider を使用して Squid を初期化しました。React Server Component は React Context に access できないため、この Squid instance は Home page 内で access できません。代わりに、Squid React SDK の install 時に自動 install される @squidcloud/client package を使用して別の instance を作成する必要があります。code の repetition を減らすため、Squid option を取得する shared utility を作成することを推奨します。
utils という folder を作成し、squid.ts という file を追加します。新しい file に次の code を追加します。
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 function を import し、SquidContextProvider の options として渡します。
import { getOptions } from "@/utils/squid";
...
export default function App({ Component, pageProps }: AppProps) {
return (
<SquidContextProvider options={getOptions()}>
<Component {...pageProps} />
</SquidContextProvider>
);
server で query した users に access できるようになったため、useQuery hook を update して initial value を受け取れるようにします。
...
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>
);
}
...
}
この code block では 2 つの change を行います。
userslist を initial data としてuseQueryhook に渡します。これにより、hook から返される initialdataは空の array ではなく user list になります。- Loading… condition を update して data が存在するかを確認します。default では、
useQueryに initial value を渡しても、client で data の query が成功するまでloadingvalue はtrueです。dataの存在を確認することで、client で query がまだ loading 中でも server からの user list を render できます。
これらの change により、page を refresh しても Loading… indicator は表示されなくなります。代わりに、page load と同時に user list が表示されます。
Server で Data を処理する
getServerSideProps 内で Squid を使用することに加えて、API route で Squid を使用できます!client ではなく API route から user を insert してみましょう。
- default では Next.js は
pages/api/hello.tsfile を生成します。この file をinsert.tsに rename します。 pages/api/insert.tsを次の code に置き換えます。
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);
}
この code は Home component の insertUser function と非常に似ていることに注目してください。どちらも built-in database に user を作成しますが、こちらは client ではなく server で実行されます。
client からこの function を call するには、insertUser function を以下に update します。
export default function Home(...) {
...
const insertUser = async () => {
await fetch("api/insert", { method: "POST" });
};
...
}
“Insert” button をクリックして user を insert します。今回は server が insert を処理します。
Optimistic Update
button の click と新しく insert された user が user list に表示される間に、小さな delay があることに注目してください。これは Squid が client で optimistic update を自動処理する仕組みによるものです。
client-side implementation の insertUser では、insert は client で直接実行されます。この場合、Squid は optimistic に insert を実行します。つまり、insert request がまだ in flight の間でも新しい user が instantaneously に表示されます。何らかの理由で insert が failure した場合、Squid は optimistic insert を rollback します。
server から insert する場合、optimistic update の benefit は失われます。一般に、API route 内の Squid を使用して insert することはできますが、client から直接 insert・update するほうが user experience が向上することがよくあります。
これで完了です!
Next.js と Squid の使用方法の学習、おめでとうございます!これらを組み合わせることで、simple かつ streamlined な web development approach を実現できます。ぜひ活用して、楽しく構築してください!