Save & publish handlers
Blok treats drafts and published versions as two separate persistence events. You decide what each one means for your app.
onSave
Fires on ⌘/Ctrl+S and the Save button.
<Blok
config={config}
onSave={async (data) => {
await fetch(`/api/pages/${pageId}/draft`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(data),
});
}}
/>The handler receives the current document from the store. You can make
it async — Blok doesn’t block the UI while it runs.
onPublish
Fires on the Publish button. Pre-publish gates (SEO, comments, custom) run first; the handler only fires if they pass.
<Blok
onPublish={async (data) => {
await fetch(`/api/pages/${pageId}/publish`, {
method: "POST",
body: JSON.stringify(data),
});
}}
/>Publish implies save — you don’t need to call your save handler separately. Most apps point Publish at a different endpoint that also handles things like cache revalidation and CDN purge.
onSchedulePublish
Fires on Publish → Schedule. Your handler is responsible for picking up the scheduled time from the UI (Blok stores the target time in component state and passes the current document to the callback).
<Blok
onSchedulePublish={async (data) => {
const scheduledFor = await askUserForTime();
await fetch(`/api/pages/${pageId}/schedule`, {
method: "POST",
body: JSON.stringify({ data, scheduledFor }),
});
}}
/>If you prefer, use @useblok/sdk’s useBlokDocument — it wires
scheduling through a BlokStorageAdapter that handles persistence for
you.
onUnpublish
Fires on Publish → Unpublish. No document is passed (the retraction is about removing what’s live).
<Blok
onUnpublish={async () => {
await fetch(`/api/pages/${pageId}/unpublish`, { method: "POST" });
}}
/>Error handling
If your handler throws, Blok won’t retry automatically and won’t show a toast. Wrap your handler in a try/catch and surface errors to the user yourself (e.g. a toast library or your own error overlay).
onSave={async (data) => {
try {
await api.saveDraft(data);
} catch (err) {
toast.error("Couldn't save — please try again.");
throw err; // re-throwing keeps the document "dirty"
}
}}Dirty state
Blok tracks a dirty flag internally: it turns true on any change
and back to false after a successful save. The useDirty() hook reads it:
import { useDirty } from "@useblok/core";
function SaveReminder() {
const dirty = useDirty();
return dirty ? <div>You have unsaved changes</div> : null;
}Beware of stale closures
If your handler references React state or props, use a ref or access them via a hook inside the handler. The same handler instance is called every time, so stale closures are a foot-gun:
// Bad — `userId` is captured at mount
const onSave = (data) => api.save(userId, data);
// Good — always reads the latest userId
const onSave = useCallback(
(data) => api.save(userIdRef.current, data),
[]
);