Skip to main content

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

  1. 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

  1. Navigate to the Squid Console and create a new application named next-tutorial.
Note

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.

  1. 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.

  2. Change to the root project directory:

cd ..
  1. 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.

  1. Change to the next-tutorial-backend directory and start the backend using squid start:
cd next-tutorial-backend
squid start
  1. 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:


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.

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

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:

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>
);
}

To render a Users component, replace all the code in app/page.tsx with the following:

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

Note

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:

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>
);

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:

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

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:

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',
};
}

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:

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>
);
}

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:

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>
);
}

...
}

This code block makes two changes:

  1. It passes the list of users to the useQuery hook as initial data. This ensures that the initial data returned from the hook will be the list of users instead of an empty array.
  2. It updates the Loading… condition to check if any data exists. By default, even if we pass an initial value to useQuery, the loading value will be true until the data has been successfully queried on the client. Checking if the data 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:

  1. The client component that accepts the query data.
  2. The query to execute.
  3. 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:

  1. Remove the useQuery hook from the component. The data is now being supplied by the wrapping withServerQuery function.
  2. Rename the users prop to data. The withServerQuery passes a data prop through to the client component that it wraps.
  3. Update the prop type to WithQueryProps<User>. This is a wrapper that essentially translates to { data: Array<User> }.
  4. Remove the if (loading) {...} and if (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:

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>
);
}

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.

Note

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:

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);
}

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:

components/users.tsx
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.

Tip

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:

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:

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 do not support optimistic updates since they run server-side.

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!