Introduction

Getting started with TanStack Start

#Automatic Setup With Create Instant App

The fastest way to get started with Instant on TanStack Start is to use npx create-instant-app to scaffold a new project with Instant already set up.

To get started run:

npx create-instant-app -b tanstack-start

#Manual Setup

The following guide demonstrates the basics for manually scaffolding a new tanstack project with Instant and managing data. In general we recommend the automatic setup above which includes all of this and a basic auth flow and other useful full-stack utilities.

Create a blank TanStack Start app:

npx @tanstack/cli create my-app

Add the InstantDB React Library:

npm i @instantdb/react

Use instant-cli to set up a new Instant project. This prompt you to log in if you haven't already., It will then create a schema file, permissions file, and update your .env file.

npx instant-cli init

Create a database client in src/lib/db.ts:

src/lib/db.ts
import { init } from '@instantdb/react';
import schema from '../instant.schema';
export const db = init({
appId: process.env.VITE_INSTANT_APP_ID!,
schema,
useDateObjects: true,
});

You're now ready to make queries and transactions to your database!

#Creating a To-Do List App

Let's add a "todo" entity to our schema file at src/instant.schema.ts:

src/instant.schema.ts
import { i } from '@instantdb/react';
const _schema = i.schema({
entities: {
$files: i.entity({
path: i.string().unique().indexed(),
url: i.string(),
}),
$users: i.entity({
email: i.string().unique().indexed().optional(),
imageURL: i.string().optional(),
type: i.string().optional(),
}),
todos: i.entity({
text: i.string(),
done: i.boolean(),
createdAt: i.date(),
}),
},
links: {
$usersLinkedPrimaryUser: {
forward: {
on: '$users',
has: 'one',
label: 'linkedPrimaryUser',
onDelete: 'cascade',
},
reverse: {
on: '$users',
has: 'many',
label: 'linkedGuestUsers',
},
},
},
rooms: {},
});
//...

Push the schema:

npx instant-cli push

Replace the content of src/routes/index.tsx with the following:

src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { AppSchema } from "../instant.schema";
import { id, InstaQLEntity } from "@instantdb/react";
import { db } from "@/lib/db";
export const Route = createFileRoute("/")({
component: App,
});
type Todo = InstaQLEntity<AppSchema, "todos">;
function App() {
// Read Data
const { isLoading, error, data } = db.useQuery({ todos: {} });
if (isLoading) {
return null;
}
if (error) {
return <div className="text-red-500 p-4">Error: {error.message}</div>;
}
const { todos } = data;
return (
<div className="p-8 max-w-2xl">
<div className="bg-white rounded-lg p-6 border border-neutral-200 shadow flex flex-col">
<h2 className="tracking-wide text-[#F54A00] pb-2 text-2xl">Todos</h2>
<div className="text-xs pb-4">
Open another tab to see todos update in realtime!
</div>
<div className="border rounded border-neutral-300">
<TodoForm />
<TodoList todos={todos} />
<ActionBar todos={todos} />
</div>
</div>
</div>
);
}
// Write Data
// ---------
function addTodo(text: string) {
db.transact(
db.tx.todos[id()].update({
text,
done: false,
createdAt: Date.now(),
}),
);
}
function deleteTodo(todo: Todo) {
db.transact(db.tx.todos[todo.id].delete());
}
function toggleDone(todo: Todo) {
db.transact(db.tx.todos[todo.id].update({ done: !todo.done }));
}
function deleteCompleted(todos: Todo[]) {
const completed = todos.filter((todo) => todo.done);
if (completed.length === 0) return;
const txs = completed.map((todo) => db.tx.todos[todo.id].delete());
db.transact(txs);
}
function TodoForm() {
return (
<div className="flex items-center h-10 border-neutral-300">
<form
className="flex-1 h-full"
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.input as HTMLInputElement;
addTodo(input.value);
input.value = "";
}}
>
<input
className="w-full h-full px-2 outline-none bg-transparent"
autoFocus
placeholder="What needs to be done?"
type="text"
name="input"
/>
</form>
</div>
);
}
function TodoList({ todos }: { todos: Todo[] }) {
return (
<div className="divide-y divide-neutral-300">
{todos.map((todo) => (
<div key={todo.id} className="flex items-center h-10">
<div className="h-full px-2 flex items-center justify-center">
<div className="w-5 h-5 flex items-center justify-center">
<input
type="checkbox"
className="cursor-pointer"
checked={todo.done}
onChange={() => toggleDone(todo)}
/>
</div>
</div>
<div className="flex-1 px-2 overflow-hidden flex items-center">
{todo.done ? (
<span className="line-through">{todo.text}</span>
) : (
<span>{todo.text}</span>
)}
</div>
<button
className="h-full px-2 flex items-center justify-center text-neutral-300 hover:text-neutral-500"
onClick={() => deleteTodo(todo)}
>
X
</button>
</div>
))}
</div>
);
}
function ActionBar({ todos }: { todos: Todo[] }) {
return (
<div className="flex justify-between items-center h-10 px-2 text-xs border-t border-neutral-300">
<div>Remaining todos: {todos.filter((todo) => !todo.done).length}</div>
<button
className=" text-neutral-300 hover:text-neutral-500"
onClick={() => deleteCompleted(todos)}
>
Delete Completed
</button>
</div>
);
}

Go to localhost:3000, and huzzah 🎉 You've got a fully functional todo list running!

#Next Steps

Want to dive deeper on how this todo app works? Check out our step-by-step Todo List Tutorial. In this tutorial we walk through how to build a todo list app from scratch, and explain how the queries and transactions work in more detail.

For the advanced use case of integrating with TanStack Query and enabling SSR, refer to our tanstack-start-with-tanstack-query example. It can be scaffolded using npx create-instant-app -b tanstack-start-with-tanstack-query.

You can also check out the Working with data section to learn more Instant concepts.

As you get more familiar with Instant, check out our Recommended Workflow docs for using Instant in your projects.