Actions
This guide covers executing actions, validation, optimistic updates, and debouncing patterns.
useOsdkAction
Execute and validate actions with automatic state management.
Basic Usage
import { completeTodo, Todo } from "@my/osdk";
import { useOsdkAction, useOsdkObject } from "@osdk/react";
import { useCallback } from "react";
function TodoView({ todo }: { todo: Todo.OsdkInstance }) {
const { isLoading } = useOsdkObject(todo);
const { applyAction, data, error, isPending } = useOsdkAction(
completeTodo,
);
const onClick = useCallback(() => {
applyAction({
todo: todo,
isComplete: true,
});
}, [applyAction, todo]);
return (
<div>
<div>
{todo.title}
{todo.isComplete === false && (
<button onClick={onClick} disabled={isPending}>
Mark Complete
</button>
)}
{isPending && "(Applying)"}
{data && "(Action completed successfully)"}
</div>
{error && (
<div>
An error occurred: {error.actionValidation?.message
?? (error.unknown ? String(error.unknown) : "Unknown error")}
</div>
)}
</div>
);
}
Return Values
applyAction- Function to execute the action (accepts single args object or array for batch)data- Return value from the last successful action executionerror- Error object (see error handling below)isPending- True while action is executingisValidating- True while validation is in progressvalidateAction- Function to validate without executingvalidationResult- Result of last validation
Error Handling
The error object has the following structure:
{
actionValidation?: ActionValidationError;
unknown?: unknown;
}
ActionValidationError extends Error and has:
message- Error message stringvalidation- Full validation response from server
Example:
import { completeTodo, Todo } from "@my/osdk";
import { useOsdkAction } from "@osdk/react";
function TodoActionWithErrorHandling({ todo }: { todo: Todo.OsdkInstance }) {
const { applyAction, error, isPending } = useOsdkAction(completeTodo);
const onClick = async () => {
try {
await applyAction({ todo, isComplete: true });
} catch (e) {
console.error("Action failed", e);
}
};
return (
<div>
<button onClick={onClick} disabled={isPending}>
Complete Todo
</button>
{error?.actionValidation && (
<div className="error">
Validation failed: {error.actionValidation.message}
</div>
)}
{error?.unknown && (
<div className="error">
An unexpected error occurred: {String(error.unknown)}
</div>
)}
</div>
);
}
Validation
Validate action parameters without executing using validateAction.
import { createTodo } from "@my/osdk";
import { useOsdkAction } from "@osdk/react";
import { useState } from "react";
function TodoForm() {
const [title, setTitle] = useState("");
const [assignee, setAssignee] = useState("");
const {
applyAction,
validateAction,
isValidating,
validationResult,
isPending,
error,
} = useOsdkAction(createTodo);
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTitle = e.target.value;
setTitle(newTitle);
validateAction({ title: newTitle, assignee });
};
const handleAssigneeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newAssignee = e.target.value;
setAssignee(newAssignee);
validateAction({ title, assignee: newAssignee });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (validationResult?.result === "VALID") {
await applyAction({ title, assignee });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={handleTitleChange}
placeholder="Todo title"
/>
<input
type="text"
value={assignee}
onChange={handleAssigneeChange}
placeholder="Assignee"
/>
{isValidating && <span>Validating...</span>}
{validationResult?.result === "INVALID" && (
<div className="error">Invalid: please check required fields</div>
)}
<button
type="submit"
disabled={isPending || isValidating
|| validationResult?.result !== "VALID"}
>
Create Todo
</button>
{error?.actionValidation && (
<div className="error">
Validation error: {error.actionValidation.message}
</div>
)}
</form>
);
}
Key features:
validateAction- Validates action parameters without executingisValidating- True while validation is in progressvalidationResult- Contains{ result: "VALID" | "INVALID", ... }- Calling
validateActionwhile a previous validation is in progress cancels the previous one - Validation and execution are mutually exclusive
Batch Actions
Apply the same action to multiple items in a single call:
import { completeTodo, Todo } from "@my/osdk";
import { useOsdkAction } from "@osdk/react";
import { useCallback } from "react";
function BulkCompleteButton({ todos }: { todos: Todo.OsdkInstance[] }) {
const { applyAction, isPending } = useOsdkAction(completeTodo);
const onClick = useCallback(() => {
applyAction(
todos.map(todo => ({
todo: todo,
isComplete: true,
})),
);
}, [applyAction, todos]);
return (
<button onClick={onClick} disabled={isPending}>
Complete All ({todos.length})
</button>
);
}
Optimistic Updates
Apply changes to the cache immediately while waiting for the server response.
import { completeTodo, Todo } from "@my/osdk";
import { useOsdkAction, useOsdkObject } from "@osdk/react";
import { useCallback } from "react";
function TodoView({ todo }: { todo: Todo.OsdkInstance }) {
const { isLoading, isOptimistic } = useOsdkObject(todo);
const { applyAction, error, isPending } = useOsdkAction(completeTodo);
const onClick = useCallback(() => {
applyAction({
todo: todo,
isComplete: true,
$optimisticUpdate: (ou) => {
ou.updateObject(todo.$clone({ isComplete: true }));
},
});
}, [applyAction, todo]);
return (
<div>
{todo.title}
{todo.isComplete === false && !isOptimistic && (
<button onClick={onClick} disabled={isPending}>Mark Complete</button>
)}
{isPending && "(Saving)"}
{isLoading && "(Loading)"}
{isOptimistic && "(Optimistic)"}
{error && (
<div className="error">
{error.actionValidation?.message
?? (error.unknown ? String(error.unknown) : "Unknown error")}
</div>
)}
</div>
);
}
How Optimistic Updates Work
- When you call
applyActionwith$optimisticUpdate, the cache is updated immediately - The UI shows the optimistic state (tracked via
isOptimistic) - If the action succeeds, the cache is refreshed with server data
- If the action fails, the optimistic changes are rolled back automatically
Optimistic Update API
The $optimisticUpdate callback receives an object with the following methods:
$optimisticUpdate: (ou) => {
ou.updateObject(todo.$clone({ isComplete: true }));
ou.updateObject(anotherObject.$clone({ ... }));
}
$clone methodEvery OSDK object instance has a $clone() method that creates a new object with modified properties. This is essential for optimistic updates because OSDK objects are immutable.
// Create a modified copy without mutating the original
const completedTodo = todo.$clone({ isComplete: true });
// Clone with multiple property changes
const updatedTodo = todo.$clone({
title: "New Title",
priority: "high",
});
Dev-Mode Action Delay
In development builds, the observable client adds a short artificial delay (1000ms by default) to an action's result when an $optimisticUpdate is provided. This makes the optimistic state visible before the server response lands, so you can confirm your optimistic updates render correctly. The delay never runs in production builds, and it is skipped entirely for actions without an optimistic update (such as function-backed actions), so those stay fast.
Tune or disable it on the provider:
// Disable the delay
<OsdkProvider client={client} devMode={{ actionDelayMs: 0 }}>
{children}
</OsdkProvider>
// Use a shorter delay
<OsdkProvider client={client} devMode={{ actionDelayMs: 200 }}>
{children}
</OsdkProvider>
The first time the delay is applied, a one-time message is logged to the console explaining what happened and how to turn it off. If you are configuring the observable client directly, the same option is available as createObservableClient(client, extraUserAgents, { devMode: { actionDelayMs: 0 } }).
useDebouncedCallback
Debounce callback functions for auto-save patterns or expensive operations.
Basic Usage
import { useDebouncedCallback } from "@osdk/react";
import { useState } from "react";
function SearchableList({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState("");
const debouncedSearch = useDebouncedCallback((q: string) => {
onSearch(q);
}, 500);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return (
<input
value={query}
onChange={handleChange}
placeholder="Search..."
/>
);
}
Auto-Save Pattern
Combine with actions for auto-saving:
import { Todo, updateTodo } from "@my/osdk";
import { useDebouncedCallback, useOsdkAction } from "@osdk/react";
import { useState } from "react";
function AutoSaveTodo({ todo }: { todo: Todo.OsdkInstance }) {
const [title, setTitle] = useState(todo.title);
const { applyAction } = useOsdkAction(updateTodo);
const debouncedSave = useDebouncedCallback((newTitle: string) => {
applyAction({
todo,
title: newTitle,
$optimisticUpdate: (ou) => {
ou.updateObject(todo.$clone({ title: newTitle }));
},
});
}, 1000);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTitle = e.target.value;
setTitle(newTitle);
debouncedSave(newTitle);
};
return (
<input
value={title}
onChange={handleChange}
placeholder="Click to edit title..."
/>
);
}
Debounced Callback Methods
The returned function has utility methods:
import { useDebouncedCallback } from "@osdk/react";
const debouncedFn = useDebouncedCallback((value: string) => {
console.log("Called with:", value);
}, 500);
debouncedFn("hello");
debouncedFn.cancel();
debouncedFn.flush();