Skip to content

Commit 9de9d06

Browse files
authored
Quickstart remix (#4113)
# Description of Changes Adds a Remix quickstart template and documentation for SpacetimeDB. ## Template (`templates/remix-ts/`) - SpacetimeDB module with TypeScript (`spacetimedb/src/index.ts`) - Remix app with Vite configuration - SpacetimeDB provider in `app/providers.tsx` with proper SSR/hydration handling - Example route (`app/routes/_index.tsx`) demonstrating `useTable` and `useReducer` hooks - Pre-generated module bindings in `src/module_bindings/` ## Documentation (`docs/docs/00100-intro/00200-quickstarts/00175-remix.md`) - Step-by-step quickstart guide following existing pattern - Covers project creation with `spacetime dev --template remix-ts` - Explains project structure, provider pattern, and React hooks usage - Documents environment variable configuration (`VITE_SPACETIMEDB_HOST`, `VITE_SPACETIMEDB_DB_NAME`) ## Consistency - Environment variables follow the same `HOST`/`DB_NAME` naming convention as other quickstarts - Documentation structure mirrors Next.js and React quickstarts - Dev server runs on port 3001 to avoid conflict with SpacetimeDB on port 3000 # API and ABI breaking changes None. # Expected complexity level and risk 1 - Additive change only. New template and documentation, no modifications to existing functionality. # Testing - [ ] Run `spacetime dev --template remix-ts my-remix-app` and verify app starts - [ ] Verify SpacetimeDB connection works at http://localhost:3001 - [ ] Test adding a person via the UI form - [ ] Verify SSR doesn't crash (SpacetimeDB provider defers to client-side) - [ ] Check documentation renders correctly in Docusaurus
1 parent 440cc19 commit 9de9d06

19 files changed

Lines changed: 845 additions & 0 deletions

File tree

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

templates/remix-ts/.template.json

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

templates/remix-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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { DbConnection, Person } from '../../src/module_bindings';
2+
import type { Infer } from 'spacetimedb';
3+
4+
const HOST = process.env.SPACETIMEDB_HOST ?? 'wss://maincloud.spacetimedb.com';
5+
const DB_NAME = process.env.SPACETIMEDB_DB_NAME ?? 'remix-ts';
6+
7+
export type PersonData = Infer<typeof Person>;
8+
9+
/**
10+
* Fetches the initial list of people from SpacetimeDB.
11+
* This function is designed for use in Remix loaders.
12+
*
13+
* It establishes a WebSocket connection, subscribes to the person table,
14+
* waits for the initial data, and then disconnects.
15+
*/
16+
export async function fetchPeople(): Promise<PersonData[]> {
17+
return new Promise((resolve, reject) => {
18+
const timeoutId = setTimeout(() => {
19+
reject(new Error('SpacetimeDB connection timeout'));
20+
}, 10000);
21+
22+
const connection = DbConnection.builder()
23+
.withUri(HOST)
24+
.withModuleName(DB_NAME)
25+
.onConnect(conn => {
26+
// Subscribe to all people
27+
conn
28+
.subscriptionBuilder()
29+
.onApplied(() => {
30+
clearTimeout(timeoutId);
31+
// Get all people from the cache
32+
const people = Array.from(conn.db.person.iter());
33+
conn.disconnect();
34+
resolve(people);
35+
})
36+
.onError((_ctx, error) => {
37+
clearTimeout(timeoutId);
38+
conn.disconnect();
39+
reject(error);
40+
})
41+
.subscribe('SELECT * FROM person');
42+
})
43+
.onConnectError((_ctx, error) => {
44+
clearTimeout(timeoutId);
45+
reject(error);
46+
})
47+
.build();
48+
});
49+
}

templates/remix-ts/app/root.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useMemo, useState, useEffect } from 'react';
2+
import {
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
} from '@remix-run/react';
9+
import type { LinksFunction } from '@remix-run/node';
10+
import { SpacetimeDBProvider } from 'spacetimedb/react';
11+
import { DbConnection, ErrorContext } from '../src/module_bindings';
12+
import { Identity } from 'spacetimedb';
13+
14+
const HOST =
15+
import.meta.env.VITE_SPACETIMEDB_HOST ?? 'wss://maincloud.spacetimedb.com';
16+
const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? 'remix-ts';
17+
18+
const onConnect = (_conn: DbConnection, identity: Identity, token: string) => {
19+
if (typeof window !== 'undefined') {
20+
localStorage.setItem('auth_token', token);
21+
}
22+
console.log(
23+
'Connected to SpacetimeDB with identity:',
24+
identity.toHexString()
25+
);
26+
};
27+
28+
const onDisconnect = () => {
29+
console.log('Disconnected from SpacetimeDB');
30+
};
31+
32+
const onConnectError = (_ctx: ErrorContext, err: Error) => {
33+
console.log('Error connecting to SpacetimeDB:', err);
34+
};
35+
36+
export const links: LinksFunction = () => [];
37+
38+
function Providers({ children }: { children: React.ReactNode }) {
39+
const [isClient, setIsClient] = useState(false);
40+
41+
useEffect(() => {
42+
setIsClient(true);
43+
}, []);
44+
45+
const connectionBuilder = useMemo(() => {
46+
if (typeof window === 'undefined') return null;
47+
return DbConnection.builder()
48+
.withUri(HOST)
49+
.withModuleName(DB_NAME)
50+
.withToken(localStorage.getItem('auth_token') || undefined)
51+
.onConnect(onConnect)
52+
.onDisconnect(onDisconnect)
53+
.onConnectError(onConnectError);
54+
}, []);
55+
56+
// During SSR or before hydration, render children without provider
57+
if (!isClient || !connectionBuilder) {
58+
return <>{children}</>;
59+
}
60+
61+
return (
62+
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
63+
{children}
64+
</SpacetimeDBProvider>
65+
);
66+
}
67+
68+
export function Layout({ children }: { children: React.ReactNode }) {
69+
return (
70+
<html lang="en">
71+
<head>
72+
<meta charSet="utf-8" />
73+
<meta name="viewport" content="width=device-width, initial-scale=1" />
74+
<Meta />
75+
<Links />
76+
</head>
77+
<body>
78+
<Providers>{children}</Providers>
79+
<ScrollRestoration />
80+
<Scripts />
81+
</body>
82+
</html>
83+
);
84+
}
85+
86+
export default function App() {
87+
return <Outlet />;
88+
}

0 commit comments

Comments
 (0)