Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9bf6329
tests passing
KyleAMathews Jun 19, 2025
e3b5762
fix db-collections tests
KyleAMathews Jun 19, 2025
041b1b0
Add changeset
KyleAMathews Jun 19, 2025
3b43c59
typo fix
KyleAMathews Jun 19, 2025
04f6a1c
update lock file
KyleAMathews Jun 19, 2025
3dcedca
prettier
KyleAMathews Jun 19, 2025
af55b05
Fix
KyleAMathews Jun 19, 2025
73cae9a
update docs
KyleAMathews Jun 19, 2025
d466a71
inline code comments
KyleAMathews Jun 19, 2025
09f027b
Always use select schemas
KyleAMathews Jun 19, 2025
d7b3b83
Fixes to example
KyleAMathews Jun 19, 2025
ad2d372
original has to match the type of the collection
KyleAMathews Jun 20, 2025
6ac1383
include path in the validation method
KyleAMathews Jun 20, 2025
e342ba9
The original object is either T or an empty object (for inserts)
KyleAMathews Jun 20, 2025
aa99595
publish previews before doing the size comparisons
KyleAMathews Jun 20, 2025
565afdc
fix test
KyleAMathews Jun 20, 2025
6b85af2
cleanup output
KyleAMathews Jun 20, 2025
af7635f
More fixes to PendingMutations
KyleAMathews Jun 20, 2025
5bcf6bd
throw error if delete is called for a key that doesn't exist
KyleAMathews Jun 20, 2025
c0a3a41
fix bug where deletes were ignored in query results
KyleAMathews Jun 20, 2025
faefeeb
debugging
KyleAMathews Jun 20, 2025
6721b92
fix insert/update sequences
KyleAMathews Jun 20, 2025
41546ba
comment & cleanup logs
KyleAMathews Jun 20, 2025
975b9d8
more precisely type original/changes
KyleAMathews Jun 20, 2025
3a47883
try again
KyleAMathews Jun 20, 2025
9d31e6d
try try again
KyleAMathews Jun 20, 2025
54c95a7
trying yet again
KyleAMathews Jun 20, 2025
c9f3ce7
fixing
KyleAMathews Jun 20, 2025
91ba348
fix types in test
KyleAMathews Jun 20, 2025
bb19cac
Throw if no checks pass
KyleAMathews Jun 23, 2025
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
8 changes: 8 additions & 0 deletions .changeset/tiny-adults-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tanstack/db-collections": patch
"@tanstack/db": patch
---

If a schema is passed, use that for the collection type.

You now must either pass an explicit type or schema - passing both will conflict.
4 changes: 2 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
uses: tanstack/config/.github/setup@main
- name: Build Packages
run: pnpm run build
- name: Publish Previews
run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*'
- name: Compressed Size Action - DB Package
uses: preactjs/compressed-size-action@v2
with:
Expand All @@ -55,5 +57,3 @@ jobs:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
pattern: "./packages/react-db/dist/**/*.{js,mjs}"
comment-key: "react-db-package-size"
- name: Publish Previews
run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*'
12 changes: 8 additions & 4 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,18 @@ You can also use:

All collections optionally support a `schema`.

If provided, this should be a [Standard Schema](https://standardschema.dev) compatible schema instance, such as a [Zod](https://zod.dev) or [Effect](https://effect.website/docs/schema/introduction/) schema.
If provided, this must be a [Standard Schema](https://standardschema.dev) compatible schema instance, such as a [Zod](https://zod.dev) or [Effect](https://effect.website/docs/schema/introduction/) schema.

The collection will use the schema for its type so if you provide a schema, you can't also pass in an explicit
type (e.g. `createCollection<Todo>()`).


#### `QueryCollection`

[TanStack Query](https://tanstack.com/query) fetches data using managed queries. Use `queryCollectionOptions` to fetch data into a collection using TanStack Query:

```ts
const todoCollection = createCollection<Todo>(queryCollectionOptions({
const todoCollection = createCollection(queryCollectionOptions({
queryKey: ['todoItems'],
queryFn: async () => fetch('/api/todos'),
getKey: (item) => item.id,
Expand All @@ -190,7 +194,7 @@ Electric's main primitive for sync is a [Shape](https://electric-sql.com/docs/gu
import { createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/db-collections'

export const todoCollection = createCollection<Todo>(electricCollectionOptions({
export const todoCollection = createCollection(electricCollectionOptions({
id: 'todos',
shapeOptions: {
url: 'https://example.com/v1/shape',
Expand All @@ -215,7 +219,7 @@ When you create the collection, sync starts automatically.
Electric shapes allow you to filter data using where clauses:

```ts
export const myPendingTodos = createCollection<Todo>(electricCollectionOptions({
export const myPendingTodos = createCollection(electricCollectionOptions({
id: 'todos',
shapeOptions: {
url: 'https://example.com/v1/shape',
Expand Down
116 changes: 60 additions & 56 deletions examples/react/todo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
} from "@tanstack/db-collections"
// import { DevTools } from "./DevTools"
import { QueryClient } from "@tanstack/query-core"
import { updateConfigSchema, updateTodoSchema } from "./db/validation"
import { selectConfigSchema, selectTodoSchema } from "./db/validation"
import type { Collection } from "@tanstack/react-db"
import type { UpdateConfig, UpdateTodo } from "./db/validation"
import type { SelectConfig, SelectTodo } from "./db/validation"
import type { FormEvent } from "react"

// API helper for todos and config
Expand All @@ -17,21 +17,21 @@ const API_BASE_URL = `http://localhost:3001/api`
const api = {
// Todo API methods
todos: {
getAll: async (): Promise<Array<UpdateTodo>> => {
getAll: async (): Promise<Array<SelectTodo>> => {
const response = await fetch(`${API_BASE_URL}/todos`)
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`)
return response.json()
},
getById: async (id: number): Promise<UpdateTodo> => {
getById: async (id: number): Promise<SelectTodo> => {
const response = await fetch(`${API_BASE_URL}/todos/${id}`)
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`)
return response.json()
},
create: async (
todo: Partial<UpdateTodo>
): Promise<{ todo: UpdateTodo; txid: number }> => {
todo: Partial<SelectTodo>
): Promise<{ todo: SelectTodo; txid: number }> => {
const response = await fetch(`${API_BASE_URL}/todos`, {
method: `POST`,
headers: { "Content-Type": `application/json` },
Expand All @@ -43,8 +43,8 @@ const api = {
},
update: async (
id: unknown,
changes: Partial<UpdateTodo>
): Promise<{ todo: UpdateTodo; txid: number }> => {
changes: Partial<SelectTodo>
): Promise<{ todo: SelectTodo; txid: number }> => {
const response = await fetch(`${API_BASE_URL}/todos/${id}`, {
method: `PUT`,
headers: { "Content-Type": `application/json` },
Expand All @@ -68,21 +68,21 @@ const api = {

// Config API methods
config: {
getAll: async (): Promise<Array<UpdateConfig>> => {
getAll: async (): Promise<Array<SelectConfig>> => {
const response = await fetch(`${API_BASE_URL}/config`)
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`)
return response.json()
},
getById: async (id: number): Promise<UpdateConfig> => {
getById: async (id: number): Promise<SelectConfig> => {
const response = await fetch(`${API_BASE_URL}/config/${id}`)
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`)
return response.json()
},
create: async (
config: Partial<UpdateConfig>
): Promise<{ config: UpdateConfig; txid: number }> => {
config: Partial<SelectConfig>
): Promise<{ config: SelectConfig; txid: number }> => {
const response = await fetch(`${API_BASE_URL}/config`, {
method: `POST`,
headers: { "Content-Type": `application/json` },
Expand All @@ -94,8 +94,8 @@ const api = {
},
update: async (
id: number,
changes: Partial<UpdateConfig>
): Promise<{ config: UpdateConfig; txid: number }> => {
changes: Partial<SelectConfig>
): Promise<{ config: SelectConfig; txid: number }> => {
const response = await fetch(`${API_BASE_URL}/config/${id}`, {
method: `PUT`,
headers: { "Content-Type": `application/json` },
Expand Down Expand Up @@ -130,12 +130,12 @@ const collectionsCache = new Map()
// Function to create the appropriate todo collection based on type
const createTodoCollection = (type: CollectionType) => {
if (collectionsCache.has(`todo`)) {
return collectionsCache.get(`todo`) as Collection<UpdateTodo>
return collectionsCache.get(`todo`) as Collection<SelectTodo>
} else {
let newCollection: Collection<UpdateTodo>
let newCollection: Collection<SelectTodo>
if (type === CollectionType.Electric) {
newCollection = createCollection(
electricCollectionOptions<UpdateTodo>({
electricCollectionOptions({
id: `todos`,
shapeOptions: {
url: `http://localhost:3003/v1/shape`,
Expand All @@ -147,10 +147,15 @@ const createTodoCollection = (type: CollectionType) => {
timestamptz: (date: string) => new Date(date),
},
},
getKey: (item) => item.id!,
schema: updateTodoSchema,
getKey: (item) => item.id,
schema: selectTodoSchema,
onInsert: async ({ transaction }) => {
const modified = transaction.mutations[0].modified
const {
id: _id,
created_at: _f,
updated_at: _ff,
...modified
} = transaction.mutations[0].modified
const response = await api.todos.create(modified)

return { txid: String(response.txid) }
Expand All @@ -165,7 +170,7 @@ const createTodoCollection = (type: CollectionType) => {
})
)

return { txid: String(txids[0].txid) }
return { txid: String(txids[0]!.txid) }
},
onDelete: async ({ transaction }) => {
const txids = await Promise.all(
Expand All @@ -177,7 +182,7 @@ const createTodoCollection = (type: CollectionType) => {
})
)

return { txid: String(txids[0].txid) }
return { txid: String(txids[0]!.txid) }
},
})
)
Expand All @@ -190,22 +195,23 @@ const createTodoCollection = (type: CollectionType) => {
refetchInterval: 3000,
queryFn: async () => {
const todos = await api.todos.getAll()
// Turn date strings into Dates if needed
// Turn date strings into Dates
return todos.map((todo) => ({
...todo,
created_at: todo.created_at
? new Date(todo.created_at)
: undefined,
updated_at: todo.updated_at
? new Date(todo.updated_at)
: undefined,
created_at: new Date(todo.created_at),
updated_at: new Date(todo.updated_at),
}))
},
getKey: (item: UpdateTodo) => item.id!,
schema: updateTodoSchema,
getKey: (item: SelectTodo) => item.id,
schema: selectTodoSchema,
queryClient,
onInsert: async ({ transaction }) => {
const modified = transaction.mutations[0].modified
const {
id: _id,
created_at: _crea,
updated_at: _up,
...modified
} = transaction.mutations[0].modified
return await api.todos.create(modified)
},
onUpdate: async ({ transaction }) => {
Expand Down Expand Up @@ -235,9 +241,9 @@ const createTodoCollection = (type: CollectionType) => {
// Function to create the appropriate config collection based on type
const createConfigCollection = (type: CollectionType) => {
if (collectionsCache.has(`config`)) {
return collectionsCache.get(`config`)
return collectionsCache.get(`config`) as Collection<SelectConfig>
} else {
let newCollection: Collection<UpdateConfig>
let newCollection: Collection<SelectConfig>
if (type === CollectionType.Electric) {
newCollection = createCollection(
electricCollectionOptions({
Expand All @@ -254,8 +260,8 @@ const createConfigCollection = (type: CollectionType) => {
},
},
},
getKey: (item: UpdateConfig) => item.id!,
schema: updateConfigSchema,
getKey: (item: SelectConfig) => item.id,
schema: selectConfigSchema,
onInsert: async ({ transaction }) => {
const modified = transaction.mutations[0].modified
const response = await api.config.create(modified)
Expand Down Expand Up @@ -286,19 +292,15 @@ const createConfigCollection = (type: CollectionType) => {
refetchInterval: 3000,
queryFn: async () => {
const configs = await api.config.getAll()
// Turn date strings into Dates if needed
// Turn date strings into Dates
return configs.map((config) => ({
...config,
created_at: config.created_at
? new Date(config.created_at)
: undefined,
updated_at: config.updated_at
? new Date(config.updated_at)
: undefined,
created_at: new Date(config.created_at),
updated_at: new Date(config.updated_at),
}))
},
getKey: (item: UpdateConfig) => item.id,
schema: updateConfigSchema,
getKey: (item: SelectConfig) => item.id,
schema: selectConfigSchema,
queryClient,
onInsert: async ({ transaction }) => {
const modified = transaction.mutations[0].modified
Expand Down Expand Up @@ -348,12 +350,12 @@ export default function App() {
q
.from({ todoCollection: todoCollection })
.orderBy(`@created_at`)
.select(`@id`, `@created_at`, `@text`, `@completed`)
.select(`@*`)
)

const { data: configData } = useLiveQuery((q) =>
q
.from({ configCollection: configCollection as Collection<UpdateConfig> })
.from({ configCollection: configCollection })
.select(`@id`, `@key`, `@value`)
)

Expand All @@ -371,7 +373,7 @@ export default function App() {
const getConfigValue = (key: string): string => {
for (const config of configData) {
if (config.key === key) {
return config.value!
return config.value
}
}
return ``
Expand All @@ -391,8 +393,11 @@ export default function App() {

// If the config doesn't exist yet, create it
configCollection.insert({
id: Math.random(),
key,
value,
created_at: new Date(),
updated_at: new Date(),
})
}

Expand Down Expand Up @@ -457,17 +462,12 @@ export default function App() {
text: newTodo,
completed: false,
id: Math.round(Math.random() * 1000000),
created_at: new Date(),
updated_at: new Date(),
})
setNewTodo(``)
}

const toggleTodo = (todo: UpdateTodo) => {
console.log(todoCollection)
todoCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed
})
}

const activeTodos = todos.filter((todo) => !todo.completed)
const completedTodos = todos.filter((todo) => todo.completed)

Expand Down Expand Up @@ -597,7 +597,11 @@ export default function App() {
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo)}
onChange={() =>
todoCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed
})
}
className="absolute left-[12px] top-0 bottom-0 my-auto h-[40px] w-[40px] cursor-pointer"
/>
<label
Expand Down
1 change: 1 addition & 0 deletions packages/db-collections/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@tanstack/db": "workspace:*",
"@tanstack/query-core": "^5.75.7",
"@standard-schema/spec": "^1.0.0",
"@tanstack/store": "^0.7.0"
},
"devDependencies": {
Expand Down
Loading