Pages
When using Squid with Next.js, you have access to the Squid client on both the server side and the client side of your application. On the server, this allows you to query data as part of your initial payload. On the client, this allows you to execute queries, mutations and stream down real time data updates.
To start, in pages/_app.tsx
, wrap the Component
in the SquidContextProvider
. Replace the placeholders with the Squid configuration options. You can find these values in the Squid Console or in your .env
file. The .env
file was automatically generated while creating the Squid backend and is located in your 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>
);
}
Query for users
To introduce client-side queries to a users
collection into the app, replace pages/index.tsx
with the following 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>
);
}
Out of the box, Squid provides a built-in database. In this example, we use the useQuery
hook in our Users
client component to run a query of the users
collection of the database. The hook accepts a query and a boolean, which indicates whether we want to subscribe to live updates to the table. Using query().dereference()
is returns the raw data of the query.
In the web app, you can now see a “Users” heading, but no users! We still need to insert a user into the collection.
If you see “Loading…” or an error message, verify that you started the Squid backend. Navigate to tutorial-backend
and run squid start
. See Running the project
Insert a user
Add a button component that triggers a function that inserts a user into the database. In pages/index.tsx
, add the following:
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>
);
The web app now shows an Insert button. Click the button to insert a user with a random ID. The database query is subscribed to live updates, allowing the user ID to appear in the list of users as soon as the button is clicked. Running collection.doc().insert(...)
persists the user data to your application’s built-in database, so upon refreshing the page, the list of users persists.
Run a query on the server
When you refresh the page, a Loading… indicator briefly appears before the list of users is displayed. This is because our Users
component is a client component, and it takes a short amount of time to query for the users on the client. With the Next.js App Router, we can run this query inside a React Server Component and pass that data from the server to the client during page load.
In pages/index.tsx
create a new static getServerSideProps
function. This function queries Squid for the initial user data, and then passes the list of users
to the initial rendering of the Home
component:
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>) {
...
}
When we first setup our app, we initialized Squid on the client with the SquidContextProvider
. This instance of Squid is not accessible inside our Home
page, since React Server Components cannot access React Context. Instead we need to create a separate instance using the @squidcloud/client
package, which is automatically installed when installing the Squid React SDK. To reduce code repetition, we recommend creating a shared utility for getting your Squid options.
Create a folder called utils
and add a filed called squid.ts
. Add the following code to the new file:
import { SquidOptions } from '@squidcloud/client';
export function getOptions(): SquidOptions {
return {
appId: 'YOUR_APP_ID',
region: 'YOUR_REGION',
environmentId: 'dev',
squidDeveloperId: 'YOUR_SQUID_DEVELOPER_ID',
};
}
You can then replace any of the options={...}
with options={getOptions()}
.
In pages/_app.tsx
, import the getOptions
function and pass it as the options
for SquidContextProvider
:
import { getOptions } from "@/utils/squid";
...
export default function App({ Component, pageProps }: AppProps) {
return (
<SquidContextProvider options={getOptions()}>
<Component {...pageProps} />
</SquidContextProvider>
);
Now that we have accessed the users
that were queried on the server, we can update the useQuery
hook to accept an 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>
);
}
...
}
This code block makes two changes:
- It passes the list of
users
to theuseQuery
hook as initial data. This ensures that the initialdata
returned from the hook will be the list of users instead of an empty array. - It updates the Loading… condition to check if any data exists. By default, even if we pass an initial value to
useQuery
, theloading
value will betrue
until the data has been successfully queried on the client. Checking if thedata
exists allows us to render the list of users from the server while the query is still loading on the client.
With these changes, refreshing the page no longer shows the Loading… indicator. Instead, the list of users is visible as soon as the page loads.
Handle data on the server
In addition to using Squid inside of getServerSideProps
, we can use Squid in API routes! Let’s insert a user from an API route instead of from the client.
- By default Next.js generates a
pages/api/hello.ts
file. Rename this fileinsert.ts
. - Replace
pages/api/insert.ts
with the following 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);
}
Notice that this code is very similar to the insertUser
function in the Home
component. It serves the same purpose--to create a user in the built-in database--but it executes on the server instead of on the client.
To call this function from your client, update the insertUser
function to the following:
export default function Home(...) {
...
const insertUser = async () => {
await fetch("api/insert", { method: "POST" });
};
...
}
Click the “Insert” button to insert a user. This time the server handles the insert.
Optimistic updates
Notice that there’s now a small delay between clicking the button and the newly inserted user appearing in the list of users. This is because of how Squid automatically handles optimistic updates on the client.
With the client-side implementation of insertUser
, the insert happens directly on the client. In this case, Squid performs the insert optimistically, meaning the new user is displayed instantaneously even while the insert request is still in flight. If for some reason the insert fails, Squid will rollback the optimistic insert.
When inserting from the server, you lose the benefit of optimistic updates. In general, although you can insert using Squid inside your API routes, it’s often a better user experience to insert and update directly from the client.