Skip to content

Commit fe9ad4c

Browse files
authored
Quickstart nextjs (#4097)
# Description of Changes Adds a new Next.js quickstart template and documentation. **New Template (`templates/nextjs-ts/`):** - Next.js 15 App Router with TypeScript SpacetimeDB module - Pre-configured `SpacetimeDBProvider` with client-side connection handling - Environment variable support (`NEXT_PUBLIC_SPACETIMEDB_HOST`, `NEXT_PUBLIC_SPACETIMEDB_DB_NAME`) - SSR-safe implementation with `"use client"` directives - Example page demonstrating `useTable` and `useReducer` hooks - Dev server runs on port 3001 to avoid conflict with SpacetimeDB on port 3000 - Pre-generated module bindings included **New Documentation (`docs/.../00200-nextjs.md`):** - 8-step quickstart consistent with other quickstarts (React, TypeScript, Rust, C#) - Covers: project creation, structure, tables/reducers, CLI testing - Next.js-specific: provider pattern, SSR considerations, env var configuration, React hooks usage **Updated `templates-list.json`:** - Added `nextjs-ts` to highlights and templates list # API and ABI breaking changes None. # Expected complexity level and risk 1 - Adds a new template and docs without modifying existing functionality. # Testing - [ ] Run `spacetime dev --template nextjs-ts my-test-app` and verify the app starts - [ ] Navigate to http://localhost:3001 and confirm the UI loads and connects - [ ] Add a person via the UI and verify it appears in the list - [ ] Verify env vars work: set `NEXT_PUBLIC_SPACETIMEDB_URI` and `NEXT_PUBLIC_SPACETIMEDB_MODULE` - [ ] Verify the quickstart doc renders correctly in the docs site - [ ] Confirm Next.js appears in the sidebar after React
1 parent 0ed052e commit fe9ad4c

22 files changed

Lines changed: 816 additions & 0 deletions
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
---
2+
title: Next.js Quickstart
3+
sidebar_label: Next.js
4+
slug: /quickstarts/nextjs
5+
hide_table_of_contents: true
6+
---
7+
8+
import { InstallCardLink } from "@site/src/components/InstallCardLink";
9+
import { StepByStep, Step, StepText, StepCode } from "@site/src/components/Steps";
10+
11+
12+
Get a SpacetimeDB Next.js app running in under 5 minutes.
13+
14+
## Prerequisites
15+
16+
- [Node.js](https://nodejs.org/) 18+ installed
17+
- [SpacetimeDB CLI](https://spacetimedb.com/install) installed
18+
19+
<InstallCardLink />
20+
21+
---
22+
23+
<StepByStep>
24+
<Step title="Create your project">
25+
<StepText>
26+
Run the `spacetime dev` command to create a new project with a SpacetimeDB module and Next.js client.
27+
28+
This will start the local SpacetimeDB server, publish your module, generate TypeScript bindings, and start the Next.js development server.
29+
</StepText>
30+
<StepCode>
31+
```bash
32+
spacetime dev --template nextjs-ts my-nextjs-app
33+
```
34+
</StepCode>
35+
</Step>
36+
37+
<Step title="Open your app">
38+
<StepText>
39+
Navigate to [http://localhost:3000](http://localhost:3000) to see your app running.
40+
41+
The `spacetime dev` command automatically configures your app to connect to SpacetimeDB via environment variables in `.env.local`.
42+
</StepText>
43+
</Step>
44+
45+
<Step title="Explore the project structure">
46+
<StepText>
47+
Your project contains both server and client code using the Next.js App Router.
48+
49+
Edit `spacetimedb/src/index.ts` to add tables and reducers. Edit `app/page.tsx` and `app/PersonList.tsx` to build your UI.
50+
</StepText>
51+
<StepCode>
52+
```
53+
my-nextjs-app/
54+
├── spacetimedb/ # Your SpacetimeDB module
55+
│ └── src/
56+
│ └── index.ts # SpacetimeDB module logic
57+
├── app/ # Next.js App Router
58+
│ ├── layout.tsx # Root layout with providers
59+
│ ├── page.tsx # Server Component (fetches initial data)
60+
│ ├── PersonList.tsx # Client Component (real-time updates)
61+
│ └── providers.tsx # SpacetimeDB provider for real-time
62+
├── lib/
63+
│ └── spacetimedb-server.ts # Server-side data fetching
64+
├── src/
65+
│ └── module_bindings/ # Auto-generated types
66+
└── package.json
67+
```
68+
</StepCode>
69+
</Step>
70+
71+
<Step title="Understand tables and reducers">
72+
<StepText>
73+
Open `spacetimedb/src/index.ts` to see the module code. The template includes a `person` table and two reducers: `add` to insert a person, and `say_hello` to greet everyone.
74+
75+
Tables store your data. Reducers are functions that modify data — they're the only way to write to the database.
76+
</StepText>
77+
<StepCode>
78+
```typescript
79+
import { schema, table, t } from 'spacetimedb/server';
80+
81+
export const spacetimedb = schema(
82+
table(
83+
{ name: 'person', public: true },
84+
{
85+
name: t.string(),
86+
}
87+
)
88+
);
89+
90+
spacetimedb.reducer('add', { name: t.string() }, (ctx, { name }) => {
91+
ctx.db.person.insert({ name });
92+
});
93+
94+
spacetimedb.reducer('say_hello', (ctx) => {
95+
for (const person of ctx.db.person.iter()) {
96+
console.info(`Hello, ${person.name}!`);
97+
}
98+
console.info('Hello, World!');
99+
});
100+
```
101+
</StepCode>
102+
</Step>
103+
104+
<Step title="Test with the CLI">
105+
<StepText>
106+
Use the SpacetimeDB CLI to call reducers and query your data directly.
107+
</StepText>
108+
<StepCode>
109+
```bash
110+
# Call the add reducer to insert a person
111+
spacetime call my-nextjs-app add Alice
112+
113+
# Query the person table
114+
spacetime sql my-nextjs-app "SELECT * FROM person"
115+
name
116+
---------
117+
"Alice"
118+
119+
# Call say_hello to greet everyone
120+
spacetime call my-nextjs-app say_hello
121+
122+
# View the module logs
123+
spacetime logs my-nextjs-app
124+
2025-01-13T12:00:00.000000Z INFO: Hello, Alice!
125+
2025-01-13T12:00:00.000000Z INFO: Hello, World!
126+
```
127+
</StepCode>
128+
</Step>
129+
130+
<Step title="Understand server-side rendering">
131+
<StepText>
132+
The SpacetimeDB SDK works both server-side and client-side. The template uses a hybrid approach:
133+
134+
- **Server Component** (`page.tsx`): Fetches initial data during SSR for fast page loads
135+
- **Client Component** (`PersonList.tsx`): Maintains a real-time WebSocket connection for live updates
136+
137+
The `lib/spacetimedb-server.ts` file provides a utility for server-side data fetching.
138+
</StepText>
139+
<StepCode>
140+
```tsx
141+
// lib/spacetimedb-server.ts
142+
import { DbConnection } from '../src/module_bindings';
143+
144+
export async function fetchPeople() {
145+
return new Promise((resolve, reject) => {
146+
const connection = DbConnection.builder()
147+
.withUri(process.env.SPACETIMEDB_HOST!)
148+
.withModuleName(process.env.SPACETIMEDB_DB_NAME!)
149+
.onConnect(conn => {
150+
conn.subscriptionBuilder()
151+
.onApplied(() => {
152+
const people = Array.from(conn.db.person.iter());
153+
conn.disconnect();
154+
resolve(people);
155+
})
156+
.subscribe('SELECT * FROM person');
157+
})
158+
.build();
159+
});
160+
}
161+
```
162+
</StepCode>
163+
</Step>
164+
165+
<Step title="Use React hooks for real-time data">
166+
<StepText>
167+
In client components, use `useTable` to subscribe to table data and `useReducer` to call reducers. The Server Component passes initial data as props for instant rendering.
168+
</StepText>
169+
<StepCode>
170+
```tsx
171+
// app/page.tsx (Server Component)
172+
import { PersonList } from './PersonList';
173+
import { fetchPeople } from '../lib/spacetimedb-server';
174+
175+
export default async function Home() {
176+
const initialPeople = await fetchPeople();
177+
return <PersonList initialPeople={initialPeople} />;
178+
}
179+
```
180+
181+
```tsx
182+
// app/PersonList.tsx (Client Component)
183+
'use client';
184+
185+
import { tables, reducers } from '../src/module_bindings';
186+
import { useTable, useReducer } from 'spacetimedb/react';
187+
188+
export function PersonList({ initialPeople }) {
189+
// Real-time data from WebSocket subscription
190+
const [people, isLoading] = useTable(tables.person);
191+
const addPerson = useReducer(reducers.add);
192+
193+
// Use server data until client is connected
194+
const displayPeople = isLoading ? initialPeople : people;
195+
196+
return (
197+
<ul>
198+
{displayPeople.map((person, i) => <li key={i}>{person.name}</li>)}
199+
</ul>
200+
);
201+
}
202+
```
203+
</StepCode>
204+
</Step>
205+
</StepByStep>
206+
207+
## Next steps
208+
209+
- See the [Chat App Tutorial](/tutorials/chat-app) for a complete example
210+
- Read the [TypeScript SDK Reference](/sdks/typescript) for detailed API docs

templates/nextjs-ts/.template.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"description": "Next.js App Router with TypeScript server",
3+
"client_lang": "typescript",
4+
"server_lang": "typescript"
5+
}

templates/nextjs-ts/LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../licenses/apache2.txt
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
import { tables, reducers } from '../src/module_bindings';
5+
import { useSpacetimeDB, useTable, useReducer } from 'spacetimedb/react';
6+
import type { PersonData } from '../lib/spacetimedb-server';
7+
8+
interface PersonListProps {
9+
initialPeople: PersonData[];
10+
}
11+
12+
export function PersonList({ initialPeople }: PersonListProps) {
13+
const [name, setName] = useState('');
14+
const [isHydrated, setIsHydrated] = useState(false);
15+
16+
const conn = useSpacetimeDB();
17+
const { isActive: connected } = conn;
18+
19+
// Subscribe to all people in the database
20+
// useTable returns [rows, isLoading] tuple
21+
const [people, isLoading] = useTable(tables.person);
22+
23+
const addReducer = useReducer(reducers.add);
24+
25+
// Once connected and loaded, we're hydrated with real-time data
26+
useEffect(() => {
27+
if (connected && !isLoading) {
28+
setIsHydrated(true);
29+
}
30+
}, [connected, isLoading]);
31+
32+
// Use server-rendered data until client is hydrated with real-time data
33+
const displayPeople = isHydrated ? people : initialPeople;
34+
35+
const addPerson = (e: React.FormEvent) => {
36+
e.preventDefault();
37+
if (!name.trim() || !connected) return;
38+
39+
// Call the add reducer with object syntax
40+
addReducer({ name: name });
41+
setName('');
42+
};
43+
44+
return (
45+
<>
46+
<div style={{ marginBottom: '1rem' }}>
47+
Status:{' '}
48+
<strong style={{ color: connected ? 'green' : 'red' }}>
49+
{connected ? 'Connected' : 'Connecting...'}
50+
</strong>
51+
</div>
52+
53+
<form onSubmit={addPerson} style={{ marginBottom: '2rem' }}>
54+
<input
55+
type="text"
56+
placeholder="Enter name"
57+
value={name}
58+
onChange={e => setName(e.target.value)}
59+
style={{ padding: '0.5rem', marginRight: '0.5rem' }}
60+
disabled={!connected}
61+
/>
62+
<button
63+
type="submit"
64+
style={{ padding: '0.5rem 1rem' }}
65+
disabled={!connected}
66+
>
67+
Add Person
68+
</button>
69+
</form>
70+
71+
<div>
72+
<h2>People ({displayPeople.length})</h2>
73+
{displayPeople.length === 0 ? (
74+
<p>No people yet. Add someone above!</p>
75+
) : (
76+
<ul>
77+
{displayPeople.map((person, index) => (
78+
<li key={index}>{person.name}</li>
79+
))}
80+
</ul>
81+
)}
82+
</div>
83+
</>
84+
);
85+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
* {
2+
box-sizing: border-box;
3+
padding: 0;
4+
margin: 0;
5+
}
6+
7+
html,
8+
body {
9+
max-width: 100vw;
10+
overflow-x: hidden;
11+
}
12+
13+
body {
14+
color: #333;
15+
background: #fafafa;
16+
}
17+
18+
a {
19+
color: inherit;
20+
text-decoration: none;
21+
}
22+
23+
@media (prefers-color-scheme: dark) {
24+
body {
25+
color: #eee;
26+
background: #111;
27+
}
28+
}

templates/nextjs-ts/app/layout.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Metadata } from 'next';
2+
import { Providers } from './providers';
3+
import './globals.css';
4+
5+
export const metadata: Metadata = {
6+
title: 'SpacetimeDB Next.js App',
7+
description: 'A Next.js app powered by SpacetimeDB',
8+
};
9+
10+
export default function RootLayout({
11+
children,
12+
}: {
13+
children: React.ReactNode;
14+
}) {
15+
return (
16+
<html lang="en">
17+
<body>
18+
<Providers>{children}</Providers>
19+
</body>
20+
</html>
21+
);
22+
}

templates/nextjs-ts/app/page.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { PersonList } from './PersonList';
2+
import { fetchPeople } from '../lib/spacetimedb-server';
3+
4+
export default async function Home() {
5+
// Fetch initial data on the server
6+
let initialPeople: Awaited<ReturnType<typeof fetchPeople>> = [];
7+
8+
try {
9+
initialPeople = await fetchPeople();
10+
} catch (error) {
11+
// If server-side fetch fails, the client will still work
12+
// This can happen if the database is not yet published
13+
console.error('Failed to fetch initial data:', error);
14+
}
15+
16+
return (
17+
<main style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
18+
<h1>SpacetimeDB Next.js App</h1>
19+
<PersonList initialPeople={initialPeople} />
20+
</main>
21+
);
22+
}

0 commit comments

Comments
 (0)