Next.js and Squid
As an unopinionated platform, Squid can be used alongside any of your favorite frontend/fullstack frameworks. If you use Next.js, you can add Squid to your project to streamline your connections to data sources, add additional security, support real-time data updates, and much more! Additionally, to better support Next.js developers, we’ve added a few hooks that make developing with Squid and Next.js a seamless experience.
TL;DR
In this tutorial you will learn how to integrate Squid into your Next.js app. This includes:
- Querying and streaming updates to your client in real-time
- Querying data from your Next.js server during page load
- Mutating data from both the client and the server
This documentation is slightly different depending on whether you’re using the Pages Router or the App Router, so be sure to select the correct option below.
Create a new Next.js app
- Create a root project directory called
next-tutorial-project
:
mkdir next-tutorial-project
Change to the next-tutorial-project
directory and create a Next.js app by running one of the following commands:
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
There are some things to note about the project:
- The project uses Typescript, but you can use JavaScript for your own projects.
- Both the Pages Router and App Router integrate with Squid. Choose your preferred method for this tutorial.
- The project has been styled with Tailwind CSS.
Once your project is created, navigate to the next-tutorial
directory and install the Squid React SDK. The Squid React SDK provides hooks and utilities for integrating Squid into your React and Next.js project.
cd next-tutorial
npm install @squidcloud/react
Create the Squid backend
- Navigate to the Squid Console and create a new application named
next-tutorial
.
Squid provides two different target environments: dev
for development and prod
for production. This tutorial uses the dev
environment since it is designed for development. For the application to work, ensure that you are using the dev
environment throughout the project. To learn more, read about Squid's environments.
In the Squid Console, navigate to the application overview page and scroll to the Backend project section. Click Initialize backend and copy the initialization command.
Change to the root project directory:
cd ..
- Initialize the backend using the command you copied from the console. The format of the command is as follows:
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
To run a Squid project locally, you must run both the client app and the backend Squid project locally.
- Change to the
next-tutorial-backend
directory and start the backend usingsquid start
:
cd next-tutorial-backend
squid start
- Open a new terminal window and change to the
next-tutorial
directory. Then run the app:
cd next-tutorial
npm run dev
The Next.js app project is now running at http://localhost:PORT, where PORT
is logged in the terminal. Because we haven't edited what is rendered on the page yet, the app displayed is the Next.js starter project.
Router
At this point, this tutorial diverges based on whether you're using Next.js with the App Router or the Pages Router. Please select the option you chose when creating the Next.js project:
- App Router
- Pages Router
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.
With the App Router, you’re able to distinguish between components rendered on the client and components rendered on the server using the use client
and use server
directives. React Server Components (RSCs) are unique as they can perform asynchronous work (such as querying data) before rendering. In this tutorial, we start off by using Squid on the client and move into using React Server Components.
To start, in app/layout.tsx
, wrap the children
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 './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
Next, create a new component that displays a list of users. Under the app
directory, create a new components
directory and a new file called users.tsx
. Add the following code to the new file:
'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>
);
}
To render a Users
component, replace all the code in app/page.tsx
with the following:
import Users from '@/components/users';
export default function Home() {
return <Users />;
}
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 components/users.tsx
, add the following:
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>
);
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 app/page.tsx
directly execute a Squid query to grab the initial user data and pass the list of users
to the initial rendering of the Users
component:
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} />;
}
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 app/layout.tsx
, import the getOptions
function and pass it as the options
for SquidContextProvider
:
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>
);
}
Now that we have accessed the users
that were queried on the server, update the useQuery
hook in the Users
component to accept an 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>
);
}
...
}
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.
Minimize duplication using withServerQuery
So far, we learned how to query data in a React Server Component, pass it to our client component, and use it as an initial query value. However, you’ll notice that the logic for our query lives in two places: in the Home
React Server Component, and in the Users
client component. This can be difficult to maintain, especially if you change the query in one location and forget to update it in another.
To avoid duplication, the Squid React SDK exposes a withServerQuery
function. This hook handles querying the data on the server, and passing it through to your client component.
Update the Home
page to use the new withServerQuery
function. This function takes three arguments:
- The client component that accepts the query data.
- The query to execute.
- Whether to subscribe to query updates.
This function then generates a Higher Order Component that can be used to render the Users
component. Update app/page.tsx
to include the new function.
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 />;
}
To work with this new function, the Users
component needs a few changes:
- Remove the
useQuery
hook from the component. The data is now being supplied by the wrappingwithServerQuery
function. - Rename the
users
prop todata
. ThewithServerQuery
passes adata
prop through to the client component that it wraps. - Update the prop type to
WithQueryProps<User>
. This is a wrapper that essentially translates to{ data: Array<User> }
. - Remove the
if (loading) {...}
andif (error) {...}
conditionals. These are no longer required as the data should be loaded before the component is rendered.
As a result, the new components/users.tsx
component now looks like this:
'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>
);
}
Reload the Next.js app. You can noew see all of your users with no Loading… indicator and no flicker of data. Additionally, clicking Insert still dynamically updates the user list.
If you want to experiment with how subscribing to queries works, try changing true
to false
in the withServerQuery
function. In this case, you’ll still see the user data on load, but inserting a user will not result in a live update of user list (the user will be present after reloading the page). This is because you are no longer subscribed to changes in the client component, and in fact the Squid query doesn’t even run on the client at all!
When not subscribed to changes, you’re effectively using Squid only on the server side, which is a completely valid use case, especially if you don’t need any real-time data updates.
Insert data on the server
In addition to querying data on the server, we can use Squid inside of router handlers and server actions. Let’s insert a user from the server.
Route handlers
Create a new router handler under app/api/insert/route.ts
and replace this route with the following 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);
}
Notice that this code is very similar to the insertUser
function in the Users
component. It essentially serves the same purpose--to create a user in the built-in database--but now it’s executing 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" });
};
...
}
Clicking the “Insert” button now inserts a user from the server!
Optimistic updates
Notice that there is now a small delay between clicking the button and the seeing the newly inserted user 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. And 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.
Server actions
In addition to Router Handler, using the App Router supports experimental Server Actions. Server Actions allow the user to write functions that can be dynamically executed on the server.
To support Server Actions, update your next.config.js
:
module.exports = {
experimental: {
serverActions: true,
},
};
Create a new folder called actions
and add a file called insert.tsx
with the following code. The use server
directive indicates that this file represents a 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(),
});
}
On the client, Server Actions can be imported and called by submitting a form
. To call the action insertUser
function we just created, update the Users
component as follows:
"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 do not support optimistic updates since they run server-side.
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.
And that's it!
Congratulations on learning how to use Next.js with Squid! Together, they offer a simple, streamlined approach to web development. Dive in and happy building!