Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions docs/docs/00100-intro/00200-quickstarts/00175-remix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
---
title: Remix Quickstart
sidebar_label: Remix
slug: /quickstarts/remix
hide_table_of_contents: true
---

import { InstallCardLink } from "@site/src/components/InstallCardLink";
import { StepByStep, Step, StepText, StepCode } from "@site/src/components/Steps";


Get a SpacetimeDB Remix app running in under 5 minutes.

## Prerequisites

- [Node.js](https://nodejs.org/) 18+ installed
- [SpacetimeDB CLI](https://spacetimedb.com/install) installed

<InstallCardLink />

---

<StepByStep>
<Step title="Create your project">
<StepText>
Run the `spacetime dev` command to create a new project with a SpacetimeDB module and Remix client.

This will start the local SpacetimeDB server, publish your module, generate TypeScript bindings, and start the Remix development server.
</StepText>
<StepCode>
```bash
spacetime dev --template remix-ts my-remix-app
```
</StepCode>
</Step>

<Step title="Open your app">
<StepText>
Navigate to [http://localhost:3001](http://localhost:3001) to see your app running.

Note: The Remix dev server runs on port 3001 to avoid conflict with SpacetimeDB on port 3000.
</StepText>
</Step>

<Step title="Explore the project structure">
<StepText>
Your project contains both server and client code using Remix with Vite.

Edit `spacetimedb/src/index.ts` to add tables and reducers. Edit `app/routes/_index.tsx` to build your UI.
</StepText>
<StepCode>
```
my-remix-app/
├── spacetimedb/ # Your SpacetimeDB module
│ └── src/
│ └── index.ts # Server-side logic
├── app/ # Remix app
│ ├── root.tsx # Root layout with providers
│ ├── providers.tsx # SpacetimeDB provider (client-only)
│ └── routes/
│ └── _index.tsx # Home page
├── src/
│ └── module_bindings/ # Auto-generated types
└── package.json
```
</StepCode>
</Step>

<Step title="Understand tables and reducers">
<StepText>
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.

Tables store your data. Reducers are functions that modify data — they're the only way to write to the database.
</StepText>
<StepCode>
```typescript
import { schema, table, t } from 'spacetimedb/server';

export const spacetimedb = schema(
table(
{ name: 'person', public: true },
{
name: t.string(),
}
)
);

spacetimedb.reducer('add', { name: t.string() }, (ctx, { name }) => {
ctx.db.person.insert({ name });
});

spacetimedb.reducer('say_hello', (ctx) => {
for (const person of ctx.db.person.iter()) {
console.info(`Hello, ${person.name}!`);
}
console.info('Hello, World!');
});
```
</StepCode>
</Step>

<Step title="Test with the CLI">
<StepText>
Use the SpacetimeDB CLI to call reducers and query your data directly.
</StepText>
<StepCode>
```bash
# Call the add reducer to insert a person
spacetime call my-remix-app add Alice

# Query the person table
spacetime sql my-remix-app "SELECT * FROM person"
name
---------
"Alice"

# Call say_hello to greet everyone
spacetime call my-remix-app say_hello

# View the module logs
spacetime logs my-remix-app
2025-01-13T12:00:00.000000Z INFO: Hello, Alice!
2025-01-13T12:00:00.000000Z INFO: Hello, World!
```
</StepCode>
</Step>

<Step title="Understand the provider pattern">
<StepText>
SpacetimeDB is client-side only — it cannot run during server-side rendering. The `app/providers.tsx` file handles this by deferring the SpacetimeDB connection until the component mounts on the client.

The template uses environment variables for configuration. Set `VITE_SPACETIMEDB_HOST` and `VITE_SPACETIMEDB_DB_NAME` to override defaults.
</StepText>
<StepCode>
```tsx
// app/providers.tsx
import { useMemo, useState, useEffect } from 'react';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection } from '../src/module_bindings';

const HOST = import.meta.env.VITE_SPACETIMEDB_HOST ?? 'ws://localhost:3000';
const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? 'my-remix-app';

export function Providers({ children }: { children: React.ReactNode }) {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

const connectionBuilder = useMemo(() => {
if (typeof window === 'undefined') return null;
return DbConnection.builder()
.withUri(HOST)
.withModuleName(DB_NAME);
}, []);

// During SSR, render children without provider
if (!isClient || !connectionBuilder) {
return <>{children}</>;
}

return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
{children}
</SpacetimeDBProvider>
);
}
```
</StepCode>
</Step>

<Step title="Use React hooks for data">
<StepText>
In your route components, use `useTable` to subscribe to table data and `useReducer` to call reducers. The SpacetimeDB hooks work seamlessly with Remix routes.
</StepText>
<StepCode>
```tsx
// app/routes/_index.tsx
import { tables, reducers } from '../../src/module_bindings';
import { useTable, useReducer } from 'spacetimedb/react';

export default function Index() {
// Subscribe to table data - returns [rows, isLoading]
const [people] = useTable(tables.person);

// Get a function to call a reducer
const addPerson = useReducer(reducers.add);

const handleAdd = () => {
// Call reducer with object syntax
addPerson({ name: 'Alice' });
};

return (
<ul>
{people.map((person, i) => <li key={i}>{person.name}</li>)}
</ul>
);
}
```
</StepCode>
</Step>
</StepByStep>

## Next steps

- See the [Chat App Tutorial](/tutorials/chat-app) for a complete example
- Read the [TypeScript SDK Reference](/sdks/typescript) for detailed API docs
5 changes: 5 additions & 0 deletions templates/remix-ts/.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Remix with TypeScript server",
"client_lang": "typescript",
"server_lang": "typescript"
}
1 change: 1 addition & 0 deletions templates/remix-ts/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../licenses/apache2.txt
55 changes: 55 additions & 0 deletions templates/remix-ts/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useMemo, useState, useEffect } from 'react';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection, ErrorContext } from '../src/module_bindings';
import { Identity } from 'spacetimedb';

const HOST = import.meta.env.VITE_SPACETIMEDB_HOST ?? 'ws://localhost:3000';
const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? 'remix-ts';

const onConnect = (_conn: DbConnection, identity: Identity, token: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token);
}
console.log(
'Connected to SpacetimeDB with identity:',
identity.toHexString()
);
};

const onDisconnect = () => {
console.log('Disconnected from SpacetimeDB');
};

const onConnectError = (_ctx: ErrorContext, err: Error) => {
console.log('Error connecting to SpacetimeDB:', err);
};

export function Providers({ children }: { children: React.ReactNode }) {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

const connectionBuilder = useMemo(() => {
if (typeof window === 'undefined') return null;
return DbConnection.builder()
.withUri(HOST)
.withModuleName(DB_NAME)
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect(onConnect)
.onDisconnect(onDisconnect)
.onConnectError(onConnectError);
}, []);

// During SSR or before hydration, render children without provider
if (!isClient || !connectionBuilder) {
return <>{children}</>;
}

return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
{children}
</SpacetimeDBProvider>
);
}
33 changes: 33 additions & 0 deletions templates/remix-ts/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { LinksFunction } from '@remix-run/node';
import { Providers } from './providers';

export const links: LinksFunction = () => [];

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Providers>{children}</Providers>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App() {
return <Outlet />;
}
Loading
Loading