The Keel plugin provides seamless integration with Keel backends, including CRUD operations, real-time sync, and automatic authentication handling.
Installation
npm install @legendapp/state @teamkeel/client-react
Usage
import { syncedKeel } from '@legendapp/state/sync-plugins/keel'
import { APIClient } from './keelClient' // Generated Keel client
const client = APIClient()
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
create: (data) => client.api.mutations.createPost(data),
update: ({ where, values }) => client.api.mutations.updatePost({ where, values }),
delete: ({ id }) => client.api.mutations.deletePost({ id }),
as: 'object',
persist: {
name: 'posts',
plugin: ObservablePersistLocalStorage
}
})
Configuration
Single Item
get
(params: KeelGetParams) => Promise<APIResult<TRemote>>
Function to fetch a single itemget: () => client.api.queries.getPost({ id: '123' })
List of Items
list
(params: KeelListParams) => Promise<APIResult<{ results: TRemote[], pageInfo?: any }>>
Function to fetch a list of items with paginationlist: (params) => client.api.queries.listPosts(params)
Filter conditions for list querieswhere: { status: 'published' }
// Dynamic
where: () => ({ authorId: currentUser$.id.get() })
Number of items to fetch (pagination limit)first: 50 // Fetch first 50 items
CRUD Operations
create
(input: Partial<TRemote>) => Promise<APIResult<TRemote>>
Function to create itemscreate: (data) => client.api.mutations.createPost(data)
update
(params: { where: any, values?: Partial<TRemote> }) => Promise<APIResult<TRemote>>
Function to update itemsupdate: ({ where, values }) =>
client.api.mutations.updatePost({ where, values })
delete
(params: { id: string }) => Promise<APIResult<string>>
Function to delete itemsdelete: ({ id }) => client.api.mutations.deletePost({ id })
Configuration
as
'object' | 'array' | 'Map'
default:"object"
How to structure the data
object: { [id]: item }
array: [item, item]
Map: Map<id, item>
changesSince
'all' | 'last-sync'
default:"all"
How to query for updates
all: Fetch all data
last-sync: Only fetch items updated since last sync
Real-time sync configurationrealtime: {
path: (action, inputs) => `${action}:${JSON.stringify(inputs)}`,
plugin: keelRealtimePlugin
}
refreshAuth
() => void | Promise<void>
Function to refresh authentication
Wait for authentication before syncing
Field indicating soft deletion
Transform data between local and remote formats
Local persistence configuration
Examples
Basic List
import { syncedKeel } from '@legendapp/state/sync-plugins/keel'
import { APIClient } from './keelClient'
const client = APIClient()
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
as: 'object'
})
// Access posts
const posts = posts$.get()
console.log(Object.values(posts))
Full CRUD
interface Post {
id: string
title: string
content: string
authorId: string
createdAt: Date
updatedAt: Date
}
const posts$ = syncedKeel<Post>({
client,
list: (params) => client.api.queries.listPosts(params),
create: (data) => client.api.mutations.createPost(data),
update: ({ where, values }) =>
client.api.mutations.updatePost({ where, values }),
delete: ({ id }) => client.api.mutations.deletePost({ id }),
as: 'object'
})
// Create
posts$['new-id'].set({
id: 'new-id',
title: 'New Post',
content: 'Content here',
authorId: 'user-123'
})
// Update
posts$['post-1'].title.set('Updated Title')
// Delete
posts$['post-1'].delete()
With Filtering
const currentUserId$ = observable('user-123')
const myPosts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
where: () => ({
authorId: currentUserId$.get()
}),
as: 'array'
})
// Only fetches posts by current user
const posts = myPosts$.get()
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
first: 20, // Fetch 20 items per page
as: 'object'
})
// Automatically handles pagination
// Fetches all pages up to 'first' limit
Single Item
const post$ = syncedKeel({
client,
get: () => client.api.queries.getPost({ id: 'post-123' }),
update: ({ where, values }) =>
client.api.mutations.updatePost({ where, values })
})
// Returns single item (not object/array)
const post = post$.get()
console.log(post.title)
With Authentication
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
requireAuth: true,
refreshAuth: async () => {
// Refresh token if expired
await client.auth.refresh()
}
})
// Waits for authentication before fetching
Incremental Sync
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
changesSince: 'last-sync', // Only fetch updates
persist: {
name: 'posts',
plugin: ObservablePersistLocalStorage,
retrySync: true
}
})
// First load: Fetches all posts
// Subsequent loads: Only fetches posts updated since last sync
// Uses updatedAt field automatically
With Real-time
import { createKeelRealtimePlugin } from './keelRealtime'
const realtimePlugin = createKeelRealtimePlugin()
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
realtime: {
path: (action, inputs) => `posts:${JSON.stringify(inputs)}`,
plugin: realtimePlugin
}
})
// Automatically subscribes to real-time updates
// Updates observable when data changes on server
Soft Deletes
interface Post {
id: string
title: string
deleted: boolean
}
const posts$ = syncedKeel<Post>({
client,
list: (params) => client.api.queries.listPosts(params),
delete: ({ id }) => client.api.mutations.deletePost({ id }),
fieldDeleted: 'deleted', // Use soft delete
as: 'object'
})
// Delete marks as deleted instead of removing
posts$['post-1'].delete()
// { id: 'post-1', deleted: true }
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
transform: {
load: (remote) => ({
...remote,
createdAt: new Date(remote.createdAt),
updatedAt: new Date(remote.updatedAt)
}),
save: (local) => ({
...local,
createdAt: local.createdAt.toISOString(),
updatedAt: local.updatedAt.toISOString()
})
}
})
Error Handling
const posts$ = syncedKeel({
client,
list: (params) => client.api.queries.listPosts(params),
create: (data) => client.api.mutations.createPost(data),
onError: (error, { source, action, retry }) => {
console.error(`Error in ${action}:`, error)
if (source === 'create' && error.message.includes('duplicate')) {
// Handle duplicate error
showNotification('Item already exists')
} else {
showNotification(`Failed to ${source}`)
}
}
})
Multi-Tenant
const tenantId$ = observable('tenant-123')
const data$ = syncedKeel({
client,
list: (params) => client.api.queries.listData(params),
where: () => ({
tenantId: tenantId$.get()
}),
persist: {
name: computed(() => `data-${tenantId$.get()}`)
}
})
// Switch tenant - automatically refetches and loads correct cache
tenantId$.set('tenant-456')
Built-in Features
Automatic Authentication
- Waits for authentication before syncing
- Automatically refreshes expired tokens
- Retries failed requests after auth refresh
Pagination Handling
- Automatically fetches all pages
- Respects
first parameter
- Uses cursor-based pagination
Error Recovery
- Handles duplicate creation errors
- Handles “not found” deletion errors
- Retries on network failures
Optimistic Updates
- Changes update local state immediately
- Syncs with server in background
- Reverts on error (optional)
Type Safety
Keel plugin has full TypeScript support:
import type { KeelObjectBase } from '@legendapp/state/sync-plugins/keel'
interface Post extends KeelObjectBase {
title: string
content: string
authorId: string
}
const posts$ = syncedKeel<Post>({
client,
list: (params) => client.api.queries.listPosts(params),
// TypeScript knows the shape of Post
})
// Type-safe access
posts$['post-1'].title.set('New Title') // ✓
posts$['post-1'].invalid.set('value') // ✗ Error
Best Practices
- Use incremental sync: Enable
changesSince: 'last-sync' for better performance
- Add persistence: Cache data locally for offline support
- Handle errors: Implement
onError for production apps
- Enable real-time: Use
realtime for live updates
- Filter at source: Use
where to reduce data transfer
See Also