Skip to Content
For DevelopersSave & publish handlers

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), [] );
Last updated on