Compare commits

...

22 Commits

Author SHA1 Message Date
Trisha Lim
87214f5cbc CoValues creation docs: remove description field 2025-02-26 15:19:43 +07:00
Trisha Lim
1d1076a601 CoValues creation docs tweaks 2025-02-26 14:09:17 +07:00
Trisha Lim
2cae6263e7 Switch headings to sentence case 2025-02-26 11:55:18 +07:00
Benjamin S. Leveritt
a7d55b76ff Adds Subscription docs
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 19:41:06 +00:00
Benjamin S. Leveritt
349b479397 Typo
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 19:40:45 +00:00
Benjamin S. Leveritt
d91900e39d Removes error handling section
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 16:57:19 +00:00
Benjamin S. Leveritt
41524253bc Removes owners
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 16:57:04 +00:00
Benjamin S. Leveritt
88dfd87c02 Fix link
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 16:03:08 +00:00
Benjamin S. Leveritt
a393ec4160 Updates metadata for latest copy
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 13:47:27 +00:00
Benjamin S. Leveritt
717f1720a6 Updates the metadata copy and examples
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 13:34:57 +00:00
Benjamin S. Leveritt
d36c5278e6 Adds reviewed copy for Reading + examples
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-25 10:45:13 +00:00
Benjamin S. Leveritt
cbf71d4295 Updates Writing examples and copy
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-24 22:18:22 +00:00
Benjamin S. Leveritt
f7dd437dac Updates creation examples
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-24 22:18:08 +00:00
Benjamin S. Leveritt
0c8d547db2 Updates examples
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-24 18:59:51 +00:00
Benjamin S. Leveritt
074fcdf5ec Fix Account and Group casing
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-23 16:04:56 +00:00
Benjamin S. Leveritt
d521f138de Removes transactions content 2025-02-21 12:22:20 +00:00
Benjamin S. Leveritt
168d51b3af Improves accuracy of examples 2025-02-21 11:47:01 +00:00
Benjamin S. Leveritt
1493254451 Adds CodeGroups, improves copy 2025-02-21 08:38:08 +00:00
Benjamin S. Leveritt
9502a7b0ed Adds drafts to nav 2025-02-20 18:49:26 +00:00
Benjamin S. Leveritt
95fa514ea5 Adds metadata and writing drafts 2025-02-20 16:52:17 +00:00
Benjamin S. Leveritt
0e67be57c7 Adds Reading draft 2025-02-20 15:50:09 +00:00
Benjamin S. Leveritt
9a97ceb575 Adds Creation draft 2025-02-20 15:48:41 +00:00
8 changed files with 1205 additions and 7 deletions

View File

@@ -665,7 +665,7 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {//
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
- Now, we can get rid of a lot of coniditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
- Now, we can get rid of a lot of conditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
{/* TODO: explain about not loaded vs not set/defined and `_refs` basics */}

View File

@@ -0,0 +1,142 @@
import { CodeGroup, ComingSoon, ContentByFramework } from "@/components/forMdx";
export const metadata = { title: "Creation & ownership" };
# Creation & ownership
CoValues are inherently collaborative, meaning multiple users and devices can edit them at the same time.
Who gets to read or change each CoValue is controlled by its owner — either an individual `Account` or a shared `Group`. This foundation of ownership is what enables Jazz applications to support real-time collaboration while maintaining proper access control.
## Creating CoValues
To define the structure of your CoValue, you start with a schema. Here's an example:
<CodeGroup>
```ts
// schema.ts
class Task extends CoMap {
title = co.string;
status = co.literal("todo", "in progress", "completed");
}
```
</CodeGroup>
From there, you can create a `Task` CoValue using the `create` method.
<CodeGroup>
```ts
const task = Task.create({
title: "Plant spring vegetables",
status: "todo",
});
```
</CodeGroup>
To learn more about defining schemas, different types of CoValues, and the primitive types you can use for the fields,
see the [schemas documentation](/docs/schemas/covalues).
When you create a CoValue, you provide its initial data and optionally specify who owns it. TypeScript will help ensure that your data matches the schema.
Here's an example of a more detailed schema:
<CodeGroup>
```ts
// Creating a CoFeed for activity notifications
class ActivityNotification extends CoMap {
message = co.string;
type = co.literal("info", "warning", "success");
timestamp = co.Date;
}
class ActivityFeed extends CoFeed.Of(co.ref(ActivityNotification)) {}
const feed = ActivityFeed.create();
// Adding an item to the feed
feed.addItem(ActivityNotification.create({
message: "New task created",
type: "info",
timestamp: new Date()
}));
```
</CodeGroup>
## Ownership & access control
Every CoValue needs an owner to control who can access it — either an `Account` or a `Group`.
In the following example, we create a `Task` that only your account `me`, can access.
<CodeGroup>
```ts
const { me } = useAccount(); // *add*
const task = Task.create({
title: "Plant spring vegetables such as peas and carrots",
status: "todo",
}, {
owner: me // *add*
});
```
</CodeGroup>
But the owner is usually a `Group`, since that lets you share with multiple people.
<CodeGroup>
```ts
const group = Group.create(); // *add*
const task = Task.create({
title: "Plant spring vegetables",
status: "todo",
}, {
owner: group // *add*
});
```
</CodeGroup>
These permission scopes let you implement common patterns like shared workspaces, personal data, or public resources.
Learn more about [Groups for permissions](/docs/groups/intro).
### Groups & Roles
Groups have members with different roles that control what they can do.
- `admin`: Full control including managing members; can't be demoted by other admins
- `writer`: Can write and read content
- `reader`: Can only read content
- `writerOnly`: Can only write to the CoValue, not read it
Here's an example of a gardening project for Spring Planting, where every member has a different role:
<CodeGroup>
```ts
// Create a group
const gardenTeam = Group.create();
// Add garden members with different roles
gardenTeam.addMember(coordinator, "admin"); // Garden coordinator manages everything
gardenTeam.addMember(gardener, "writer"); // Gardeners can update tasks
gardenTeam.addMember(visitor, "reader"); // Visitors can view progress
// Create a list of tasks with the same owner
const taskList = ListOfTasks.create([]);
// Create a garden project with nested tasks, all with the same ownership
const springProject = Project.create({
name: "Spring Planting",
tasks: taskList
});
// Add tasks to the list
taskList.push(Task.create({
title: "Start tomato seedlings",
status: "todo"
});
```
</CodeGroup>
For more information on groups and roles, see the [Groups](/docs/groups/intro) documentation.

View File

@@ -0,0 +1,269 @@
import { CodeGroup } from "@/components/forMdx";
export const metadata = { title: "Metadata & time-travel" };
# Metadata & time-travel
One of Jazz's most powerful features is that every CoValue automatically tracks its complete edit history. This means you can see who changed what and when, examine the state of your data at any point in time, and build features like audit logs, activity feeds, and undo/redo functionality. This page explores how to access and work with the rich metadata that comes with every CoValue.
## Understanding edit history
Every CoValue in Jazz maintains a full history of all changes made to it. This edit history is accessible through two main APIs:
`CoValue._edits` provides a structured, field-by-field view of a CoValue's edit history. It organizes edits by property name and makes them easily accessible. For each field:
- `_edits.fieldName` gives you the most recent edit
- `_edits.fieldName.all` provides all historical edits as an array
- `_edits.fieldName.madeAt` gives you the timestamp of the last edit
- Each edit contains the value, who made the change, and when it happened
`CoValue._raw` gives you access to the internal state and lower-level operations on a CoValue. As this is an internal API, it should be used with caution. If you find yourself using `_raw`, consider letting us know so we can consider adding a public API for your use case.
## Working with edit history metadata
CoValues track who made each change and when. Every edit has metadata attached to it, including the author, timestamp, value, and transaction ID. This metadata enables you to build powerful audit and history features without having to implement your own tracking system.
<CodeGroup>
```ts
class Task extends CoMap {
title = co.string;
description = co.string;
status = co.literal("todo", "in-progress", "completed");
priority = co.literal("low", "medium", "high");
subtasks = co.optional.ref(ListOfTasks);
}
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
const task = Task.create({
title: "Plant spring vegetables",
description: "Plant peas, carrots, and lettuce in the south garden bed",
status: "todo",
priority: "medium",
});
// Change the status
task.status = "in-progress";
// Get the latest edit for a field
console.log("Latest edit:", task._edits.status);
// { value: "in-progress", by: Account, madeAt: Date, ... }
// Get when a field was last edited (timestamp)
const lastEditTime = task._edits.status.madeAt;
console.log(`Status was last changed at: ${lastEditTime?.toLocaleString()}`);
// Get the full edit history for a field
for (const edit of task._edits.status.all) {
console.log({
author: edit.by, // Account that made the change
timestamp: edit.madeAt, // When the change happened
value: edit.value, // Value of the change
});
}
```
</CodeGroup>
### Common patterns
With knowledge of the edit history, you can build all sorts of useful features that enhance your application's user experience and administrative capabilities. Here are some common patterns that leverage CoValue metadata.
#### Audit log
Getting all the changes to a CoValue in order allows you to build an audit log. This is especially useful for tracking important changes in collaborative environments or for compliance purposes:
<CodeGroup>
```ts
function getAuditLog(task: Task) {
const changes = [];
for (const field of Object.keys(task)) {
// Check if the field has edits to avoid accessing non-existent properties
if (task._edits[field as keyof typeof task._edits]) {
for (const edit of task._edits[field as keyof typeof task._edits].all) {
changes.push({
field,
...edit,
timestamp: edit.madeAt,
at: edit.madeAt,
by: edit.by,
});
}
}
}
// Sort by timestamp
return changes.sort((a, b) => b.at.getTime() - a.at.getTime());
}
// Example usage
const auditLog = getAuditLog(task);
auditLog.forEach((entry) => {
console.log(
`${entry.timestamp} - ${entry.field} changed to "${entry.value}" by ${entry.by?.id}`,
);
});
```
</CodeGroup>
#### Activity feeds
Activity feeds are a great way to see recent changes to a CoValue, helping users understand what's happening in a collaborative workspace. They can show who did what and when, creating transparency in team environments:
<CodeGroup>
```ts
function getRecentActivity(project: Project) {
const activity = [];
const hourAgo = new Date(Date.now() - 3600000);
for (const field of Object.keys(project)) {
// Skip if the field doesn't have edits
if (!project._edits[field as keyof typeof project._edits]) continue;
for (const edit of project._edits[field as keyof typeof project._edits].all) {
if (edit.madeAt > hourAgo) {
activity.push({
field,
value: edit.value,
by: edit.by,
at: edit.madeAt
});
}
}
}
return activity.sort((a, b) => b.at.getTime() - a.at.getTime());
}
// Example usage
const recentActivity = getRecentActivity(gardenProject);
console.log("Recent Garden Activity:");
recentActivity.forEach(activity => {
console.log(`${activity.at.toLocaleString()} - ${activity.field} updated by ${activity.by?.id}`);
});
```
</CodeGroup>
## Edit history & time travel
CoValues track their entire history of changes, creating a timeline you can explore. You can see who changed what and when, or even view past states of the data. This capability enables powerful debugging tools and user-facing features like history browsing and restoration of previous versions:
<CodeGroup>
```ts
class Task extends CoMap {
title = co.string;
description = co.string;
status = co.literal("todo", "in-progress", "completed");
priority = co.literal("low", "medium", "high");
}
// Create a new task
const task = Task.create({
title: "Plant spring vegetables",
description: "Plant peas, carrots, and lettuce in the south garden bed",
status: "todo",
priority: "medium",
});
// Make some changes
task.status = "in-progress";
task.priority = "high";
// See all edits for a field
for (const edit of task._edits.status.all) {
console.log(
`${edit.madeAt.toISOString()}: Status changed to "${edit.value}" by ${edit.by?.id}`,
);
}
// Get the initial value
const initialStatus = task._edits.status.all[0]?.value;
console.log(`Original status: ${initialStatus}`);
// Get a specific edit by index
const previousEdit = task._edits.status.all[1]; // Second edit
console.log(`Previous status: ${previousEdit?.value}`);
// Check who made the most recent change
const latestEdit = task._edits.status;
console.log(`Latest change made by: ${latestEdit?.by?.id}`);
```
</CodeGroup>
## Time travel
The ability to view a CoValue as it existed at any point in time is one of Jazz's most powerful features. Looking into the past can help you understand how things changed - perfect for audit logs, debugging, or showing user activity. You can reconstruct the exact state of any CoValue at any moment in its history:
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
status = co.literal("planning", "active", "completed");
lastUpdate = co.Date;
}
// See when a project was started
function findStatusChange(project: Project, targetStatus: string) {
// Get all the edits for the status field
const statusEdits = project._edits.status.all;
for (const edit of statusEdits) {
if (edit.value === targetStatus) {
console.log({
changeTime: edit.madeAt,
lastUpdate: project.lastUpdate,
changedBy: edit.by,
});
}
}
}
// Example usage
findStatusChange(gardenProject, "active");
```
</CodeGroup>
### Common use cases
The time travel capabilities of CoValues enable several practical use cases that would otherwise require complex custom implementations. Here are some examples of how you can use time travel in your applications:
<CodeGroup>
```ts
// Track task progress over time
function getTaskStatusHistory(task: Task, days: number = 7) {
const statusHistory = [];
const dayInMs = 86400000;
// Check every day for the past week
for (let day = 0; day < days; day++) {
const timePoint = new Date(Date.now() - day * dayInMs);
// Using the internal _raw API to get state at a specific point in time
const state = task._raw.atTime(timePoint);
statusHistory.push({
date: timePoint.toLocaleDateString(),
status: state.status,
priority: state.priority
});
}
return statusHistory;
}
// Example usage
const history = getTaskStatusHistory(plantingTask);
history.forEach(entry => {
console.log(`${entry.date}: Status was "${entry.status}" with ${entry.priority} priority`);
});
```
</CodeGroup>
### Best practices
- Check field existence before accessing edits (`if (task._edits.fieldName)`)
- Access the most recent edit directly with `_edits.fieldName` instead of using any `.latest` property
- Cache historical queries if you're displaying them in UI
- Be specific about time ranges you care about
- Remember that accessing history requires loading the CoValue
- Consider using timestamps from your data rather than scanning all edits
Time travel is great for understanding how you got here, but keep queries focused on the range of time that matters to your use case.

View File

@@ -0,0 +1,272 @@
import { CodeGroup, ComingSoon, ContentByFramework } from "@/components/forMdx";
export const metadata = { title: "Reading from CoValues" };
# Reading from CoValues
Jazz lets you access your collaborative data with familiar JavaScript patterns while providing TypeScript type safety. Once you have a CoValue, you can read its values, traverse references, and iterate through collections using the same syntax you'd use with regular objects and arrays. This page covers how to read from different types of CoValues and handle loading states effectively.
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
tasks = co.ref(ListOfTasks);
lead = co.optional.ref(TeamMember);
status = co.literal("planning", "active", "completed");
}
// Reading basic fields
console.log(project.name); // "Spring Garden Planning"
console.log(project.status); // "active"
// Reading from lists
for (const task of project.tasks) {
console.log(task.title); // "Plant tomato seedlings"
}
// Checking if an optional field exists
if (project.lead) {
console.log(project.lead.name); // "Maria Chen"
}
```
</CodeGroup>
## Different types of CoValues
Jazz provides several CoValue types to represent different kinds of data. Each type has its own access patterns, but they all maintain the familiar JavaScript syntax you already know.
### CoMaps
`CoMap`s work like JavaScript objects, providing named properties you can access with dot notation. These are the most common CoValue type and form the foundation of most Jazz data models:
<CodeGroup>
```ts
class TeamMember extends CoMap {
name = co.string;
role = co.string;
active = co.boolean;
}
console.log(member.name); // "Maria Chen"
console.log(member.role); // "Garden Coordinator"
console.log(member.active); // true
```
</CodeGroup>
### CoLists
`CoList`s work like JavaScript arrays, supporting indexed access, iteration methods, and length properties. They're perfect for ordered collections of items where the order matters:
<CodeGroup>
```ts
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
// Access items by index
console.log(tasks[0].title); // "Plant tomato seedlings"
// Use array methods
tasks.forEach(task => {
console.log(task.title); // "Plant tomato seedlings"
});
// Get list length
console.log(tasks.length); // 3
```
</CodeGroup>
### CoFeeds
`CoFeed`s provide a specialized way to track data from different sessions (tabs, devices, app instances). They're ideal for activity logs, presence indicators, or other session-specific streams of information. Each account can have multiple sessions, and each session maintains its own append-only log.
## Type safety with CoValues
CoValues are fully typed in TypeScript, giving you the same autocomplete and error checking you'd expect from regular objects. This type safety helps catch errors at compile time rather than runtime, making your application more robust. Here's how the type system works with CoValues:
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
memberCount = co.number;
priority = co.literal("low", "medium", "high");
lead = co.optional.ref(TeamMember);
tasks = co.ref(ListOfTasks);
}
// TypeScript knows exactly what fields exist
const project = await Project.load(gardenProjectId);
project.name = "Community Garden"; // ✓ string
project.memberCount = "few"; // ✗ Type error: expected number
project.priority = "urgent"; // ✗ Type error: must be low/medium/high
// Optional fields are handled safely
if (project.lead) {
console.log(project.lead.name); // Type safe
}
// Lists with specific item types
project.tasks.forEach(task => {
// TypeScript knows each task's structure
console.log(`${task.title}: ${task.status}`); // "Plant herbs: in-progress"
});
```
</CodeGroup>
## Loading states
When you load a CoValue, it might not be immediately available due to network latency or data size. Jazz provides patterns to handle these loading states gracefully, and TypeScript helps ensure you check for availability before accessing properties:
<CodeGroup>
```ts
const project = await Project.load(gardenProjectId);
if (!project) {
return "Data still loading";
}
```
</CodeGroup>
<ContentByFramework framework="react">
And in React, `useCoState` provides a similar pattern to allow you to wait for a CoValue to be loaded before accessing it:
<CodeGroup>
```tsx
// Type shows this might be `undefined` while loading
const project = useCoState(Project, gardenProjectId, {
tasks: [{}]
});
if (!project) {
return <div>Loading project data...</div>;
}
// TypeScript now knows project exists and has tasks loaded
return <div>{project.tasks.length}</div>;
```
</CodeGroup>
</ContentByFramework>
### Accessing nested CoValues
Nested CoValues need special handling for loading and access. Since each reference might need to be loaded separately, you need patterns to manage these dependencies and handle loading states appropriately throughout your object graph.
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
tasks = co.ref(ListOfTasks);
resources = co.optional.ref(ResourceList);
}
class Task extends CoMap {
title = co.string;
status = co.literal("todo", "in-progress", "completed");
subtasks = co.ref(ListOfSubtasks);
}
```
</CodeGroup>
### Loading
Loading nested data efficiently is important for performance. Jazz provides depth specifications to control exactly how much of your object graph is loaded, from shallow loading of just the top-level object to deep loading of complex nested structures:
<CodeGroup>
```ts
// Basic load - tasks won't be loaded yet
const project = await Project.load(gardenProjectId);
// Load with nested data
const projectWithTasks = await Project.load(gardenProjectId, { tasks: {} });
// Deep load pattern
const fullyLoaded = await Project.load(gardenProjectId, {
tasks: {
subtasks: {}
}
});
```
</CodeGroup>
More details on loading and subscribing to CoValues can be found in [Subscribing](/docs/using-covalues/subscribing-and-deep-loading).
### Handling loading states
Unloaded references return `undefined`. This means you need to check for undefined values before trying to access properties of nested CoValues.
For general JavaScript/TypeScript usage, here's a pattern that works across any context:
<CodeGroup>
```ts
// Generic pattern for handling nested data
function processTaskData(project) {
// Check if project and its tasks are loaded
if (!project || !project.tasks) {
return "Data still loading";
}
// Safe to process tasks
const completedTasks = project.tasks.filter(task =>
task && task.status === "completed"
);
// Check for subtasks before accessing them
const subtaskCount = completedTasks.reduce((count, task) => {
if (!(task && task.subtasks)) return count
return count + task.subtasks.length;
}, 0);
return {
completedCount: completedTasks.length,
subtaskCount: subtaskCount
};
}
```
</CodeGroup>
<ContentByFramework framework="react">
Handle these loading states in your components:
<CodeGroup>
```tsx
// React pattern for handling nested data
function TaskList({ project }: { project: Project }) {
if (!project.tasks) {
return <div>Loading tasks...</div>;
}
return (
<div>
{project.tasks.map(task => {
// Handle potentially missing nested data
if (!task.subtasks) {
return <div key={task.id}>Loading subtasks...</div>;
}
return (
<div key={task.id}>
{task.title}: {task.subtasks.length} subtasks
</div>
);
})}
</div>
);
}
```
</CodeGroup>
</ContentByFramework>
Note: We're working on making these patterns more explicit and robust. We'll provide clearer loading states and better error handling patterns. For now, be defensive with your checks for `undefined`.
<CodeGroup>
```ts
// Current safest pattern for deep access
function getSubtasks(project: Project, taskTitle: string) {
const task = project.tasks?.find(t => t.title === taskTitle);
const subtasks = task?.subtasks;
if (!subtasks) {
return null; // Could mean loading or error
}
return subtasks.map(st => st.title);
}
```
</CodeGroup>
Stay tuned for updates to this API - we're working on making these patterns more robust and explicit.

View File

@@ -0,0 +1,340 @@
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
export const metadata = { title: "Subscriptions & Deep Loading" };
# Subscriptions & deep loading
When working with collaborative applications, you need to know when data changes and ensure you have all the necessary related data. Jazz provides powerful subscription and deep loading capabilities that make it easy to keep your UI in sync with the underlying data and efficiently load complex object graphs.
## Understanding subscriptions
Subscriptions in Jazz allow you to react to changes in CoValues. When a CoValue changes, all subscribers are notified with the updated value. This is essential for building reactive UIs that stay in sync with collaborative data.
<CodeGroup>
```ts
class Task extends CoMap {
title = co.string;
description = co.string;
status = co.literal("todo", "in-progress", "completed");
assignedTo = co.optional.string;
}
// ...
// Subscribe to a Task by ID
const unsubscribe = Task.subscribe(taskId, { /* loading depth */ }, (updatedTask) => {
console.log("Task updated:", updatedTask.title);
console.log("New status:", updatedTask.status);
});
// Later, when you're done:
unsubscribe();
```
</CodeGroup>
### Static vs. Instance subscriptions
There are two main ways to subscribe to CoValues:
1. **Static Subscription** - When you have an ID but don't have the CoValue loaded yet:
<CodeGroup>
```ts
// Subscribe by ID (static method)
const unsubscribe = Task.subscribe(taskId, { /* loading depth */ }, (task) => {
if (task) {
console.log("Task loaded/updated:", task.title);
}
});
```
</CodeGroup>
2. **Instance Subscription** - When you already have a CoValue instance:
<CodeGroup>
```ts
// Subscribe to an instance (instance method)
const task = Task.create({
status: "todo",
title: "Cut the grass",
});
if (task) {
const unsubscribe = task.subscribe({ /* loading depth */ }, (updatedTask) => {
console.log("Task updated:", updatedTask.title);
});
}
```
</CodeGroup>
## Deep loading
When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load.
### Loading depth specifications
Loading depth specifications let you declare exactly which references to load and how deep to go:
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
tasks = co.ref(ListOfTasks);
owner = co.ref(TeamMember);
}
class Task extends CoMap {
title = co.string;
subtasks = co.ref(ListOfSubtasks);
assignee = co.optional.ref(TeamMember);
}
// Load just the project, not its tasks
const project = await Project.load(projectId, {});
// Load the project and its tasks (but not subtasks)
const projectWithTasks = await Project.load(projectId, {
tasks: {}
});
// Load the project, its tasks, and their subtasks
const projectDeep = await Project.load(projectId, {
tasks: {
subtasks: {}
}
});
// Load the project, its tasks, and task assignees
const projectWithAssignees = await Project.load(projectId, {
tasks: {
assignee: {}
}
});
// Complex loading pattern: load project, tasks with their subtasks, and the project owner
const fullyLoaded = await Project.load(projectId, {
tasks: {
subtasks: {}
},
owner: {}
});
```
</CodeGroup>
The depth specification object mirrors the structure of your data model, making it intuitive to express which parts of the graph you want to load.
### Array Notation for Lists
For lists, you can use array notation to specify how to load the items:
<CodeGroup>
```ts
// Load project with all tasks but load each task shallowly
const project = await Project.load(projectId, {
tasks: [{}]
});
// Load project with tasks and load subtasks for each task
const project = await Project.load(projectId, {
tasks: [{
subtasks: [{}]
}]
});
```
</CodeGroup>
## Framework Integration
<ContentByFramework framework="react">
### React integration with useCoState
In React applications, the `useCoState` hook provides a convenient way to subscribe to CoValues and handle loading states:
<CodeGroup>
```tsx
function GardenPlanner({ projectId }: { projectId: ID<Project> }) {
// Subscribe to a project and its tasks
const project = useCoState(Project, projectId, {
tasks: [{}]
});
// Handle loading state
if (!project) {
return <div>Loading garden project...</div>;
}
return (
<div>
<h1>{project.name}</h1>
<TaskList tasks={project.tasks} />
</div>
);
}
function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<span>{task.title}</span>
<span>{task.status}</span>
</li>
))}
</ul>
);
}
```
</CodeGroup>
The `useCoState` hook handles subscribing when the component mounts and unsubscribing when it unmounts, making it easy to keep your UI in sync with the underlying data.
</ContentByFramework>
<ContentByFramework framework="vue">
### Vue integration
In Vue applications, you can use the `useCoState` composable to subscribe to CoValues:
<CodeGroup>
```vue
<script setup>
import { useCoState } from 'jazz-vue';
const props = defineProps({
projectId: String
});
// Subscribe to a project and its tasks
const project = useCoState(Project, props.projectId, {
tasks: [{}]
});
</script>
<template>
<div v-if="project">
<h1>{{ project.name }}</h1>
<ul>
<li v-for="task in project.tasks" :key="task.id">
{{ task.title }} - {{ task.status }}
</li>
</ul>
</div>
<div v-else>
Loading garden project...
</div>
</template>
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="svelte">
### Svelte integration
In Svelte applications, you can use the `useCoState` function to subscribe to CoValues:
<CodeGroup>
```svelte
<script>
import { useCoState } from 'jazz-svelte';
export let projectId;
// Subscribe to a project and its tasks
const project = useCoState(Project, projectId, {
tasks: [{}]
});
</script>
{#if $project}
<h1>{$project.name}</h1>
<ul>
{#each $project.tasks as task (task.id)}
<li>{task.title} - {task.status}</li>
{/each}
</ul>
{:else}
<div>Loading garden project...</div>
{/if}
```
</CodeGroup>
</ContentByFramework>
## Ensuring data is loaded
Sometimes you need to make sure data is loaded before proceeding with an operation. The `ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth:
<CodeGroup>
```ts
async function completeAllTasks(projectId: ID<Project>) {
// Ensure the project and its tasks are loaded
const project = await Project.load(projectId, {});
if (!project) return;
const loadedProject = await project.ensureLoaded({
tasks: [{}]
});
// Now we can safely access and modify tasks
loadedProject.tasks.forEach(task => {
task.status = "completed";
});
}
```
</CodeGroup>
## Performance considerations
Loading depth is directly related to performance. Loading too much data can slow down your application, while loading too little can lead to "undefined" references. Here are some guidelines:
- **Load only what you need** for the current view or operation
- **Preload data** that will be needed soon to improve perceived performance
- Use **caching** to avoid reloading data that hasn't changed
{/* TODO: Add a note about supporting pagination */}
<CodeGroup>
```ts
// Bad: Loading everything deeply
const project = await Project.load(projectId, {
tasks: [{
subtasks: [{
comments: [{}]
}]
}],
members: [{}],
resources: [{}]
});
// Better: Loading only what's needed for the current view
const project = await Project.load(projectId, {
tasks: [{}] // Just load the tasks shallowly
});
// Later, when a task is selected:
const task = await Task.load(selectedTaskId, {
subtasks: [{}] // Now load its subtasks
});
```
</CodeGroup>
## Using a loading cache
By default, Jazz maintains a cache of loaded CoValues to avoid unnecessary network requests. This means that if you've already loaded a CoValue, subsequent load requests will use the cached version unless you explicitly request a refresh.
<CodeGroup>
```ts
// First load: fetches from network or local storage
const project = await Project.load(projectId, {});
// Later loads: uses cached version if available
const sameProject = await Project.load(projectId, {});
```
</CodeGroup>
## Best practices
1. **Be explicit about loading depths**: Always specify exactly what you need
2. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done
3. **Handle loading states**: Check for undefined/null before accessing properties
4. **Use framework integrations**: They handle subscription lifecycle automatically
5. **Balance depth and performance**: Load only what you need for the current view
By effectively using subscriptions and deep loading, you can build responsive, collaborative applications that handle complex data relationships while maintaining good performance.

View File

@@ -0,0 +1,175 @@
export const metadata = { title: "Writing & deleting CoValues" };
import { CodeGroup } from "@/components/forMdx";
# Writing & deleting CoValues
Collaborative applications need ways to update and remove data. Jazz makes this simple by treating CoValues like regular JavaScript objects while handling all the complexity of syncing changes in the background. This page covers how to modify CoValues, work with collections, handle concurrent edits, and properly remove data when needed.
## Writing to CoValues
Once you have a CoValue, modifying it is straightforward. You can update fields like regular JavaScript properties. Changes are applied locally first for immediate feedback, then synchronized to other users with access to the same CoValues. This approach provides a natural programming model while handling all the distributed systems complexity behind the scenes.
<CodeGroup>
```ts
class Task extends CoMap {
title = co.string;
status = co.literal("todo", "in-progress", "completed");
assignee = co.optional.string;
}
//...
// Update fields
task.status = "in-progress"; // Direct assignment
task.assignee = "Alex"; // Optional field
```
</CodeGroup>
### Working with lists
CoLists support familiar array operations, making it easy to work with collections of data. You can add, remove, and modify items using the standard JavaScript array methods, while Jazz handles the collaborative aspects automatically. These operations work correctly even when multiple users are making changes simultaneously.
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
tasks = co.ref(ListOfTasks);
}
//...
// Add items
project.tasks.push(Task.create({
title: "Build raised beds",
status: "todo"
}));
// Remove items
project.tasks.splice(2, 1); // Remove third task
// Update items
project.tasks[0].status = "in-progress";
// Bulk updates
project.tasks.forEach(task => {
if (task.status === "todo") {
task.status = "in-progress";
}
});
```
</CodeGroup>
Changes sync automatically to everyone with access. Any edits you make are immediately visible in your local view and propagate to other users as they sync.
## Concurrent edits
CoValues use [CRDTs](/docs/schemas/covalues#defining-schemas-covalues) to handle concurrent edits smoothly. In most cases, you don't need to think about conflicts - Jazz handles them automatically. This conflict resolution happens transparently, allowing multiple users to make changes simultaneously without disruption or data loss.
<CodeGroup>
```ts
class Dashboard extends CoMap {
activeProjects = co.number;
status = co.literal("active", "maintenance");
notifications = co.ref(ListOfNotifications);
}
//...
// Multiple users can edit simultaneously
// Last-write-wins for simple fields
dashboard.status = "maintenance"; // Local change is immediate
dashboard.activeProjects = 5; // Syncs automatically
// Lists handle concurrent edits too
dashboard.notifications.push(Notification.create({
timestamp: new Date(),
message: "System update scheduled"
}));
```
</CodeGroup>
## Deleting CoValues
There are a few ways to delete CoValues, from simple field removal to full cleanup. Jazz provides flexible options for removing data depending on your needs. You can remove references while keeping the underlying data, remove items from lists, or completely delete CoValues when they're no longer needed.
<CodeGroup>
```ts
class Project extends CoMap {
tasks = co.ref(ListOfTasks);
resources = co.optional.ref(ListOfResources);
}
//...
// Remove a reference
project.resources = null; // Removes the reference but resources still exist
// Remove from a list
project.tasks.splice(2, 1); // Removes third team member from list
```
</CodeGroup>
### Best practices
- Load everything you plan to delete
- Check permissions before attempting deletes
- Consider soft deletes for recoverable data
## Removing data in CoValues
You can delete fields from any `CoMap` to remove specific properties while keeping the CoValue itself. This is useful when you need to clear certain data without affecting the rest of your object structure. The deletion operations are also synchronized to all users with access.
<CodeGroup>
```ts
class Project extends CoMap {
name = co.string;
team = co.ref(ListOfMembers);
budget = co.optional.ref(Budget);
}
//...
// Delete fields from a regular CoMap
delete project.budget; // Removes the budget reference
// Delete from a record-type CoMap
class ProjectTags extends CoMap.Record(co.string) {}
const projectTags = ProjectTags.create({
"priority-high": "High priority tasks",
});
delete projectTags["priority-high"]; // Removes specific tag
```
</CodeGroup>
For `CoList`s, use array methods:
<CodeGroup>
```ts
// Remove from lists using splice
project.team.splice(2, 1); // Removes third team member
```
</CodeGroup>
### Restoring data
For data you might want to restore later, consider using status fields instead of permanent deletion. This "soft delete" pattern is common in applications where users might need to recover previously removed items. By using a boolean field to mark items as archived or deleted, you maintain the ability to restore them later.
<CodeGroup>
```ts
class Task extends CoMap {
title = co.string;
archived = co.boolean;
}
// Mark as archived
task.archived = true;
// Restore later
task.archived = false; // Task is back in the active list!
```
</CodeGroup>
Removed data remains in the edit history. If you need to handle sensitive information, plan your data model accordingly.

View File

@@ -108,27 +108,27 @@ export const docNavigationItems = [
{
name: "Creation & ownership",
href: "/docs/using-covalues/creation",
done: 0,
done: 80,
},
{
name: "Reading",
href: "/docs/using-covalues/reading",
done: 0,
done: 80,
},
{
name: "Subscribing & deep loading",
href: "/docs/using-covalues/subscription-and-loading",
done: 0,
done: 80,
},
{
name: "Writing & deleting",
href: "/docs/using-covalues/writing",
done: 0,
done: 80,
},
{
name: "Metadata & time-travel",
href: "/docs/using-covalues/metadata",
done: 0,
done: 80,
},
],
},

View File

@@ -37,7 +37,7 @@ const config = {
function highlightPlugin() {
return async function transformer(tree) {
const highlighter = await getHighlighter({
langs: ["typescript", "bash", "tsx", "json", "svelte"],
langs: ["typescript", "bash", "tsx", "json", "svelte", "vue"],
theme: "css-variables", // use css variables in shiki.css
});