Skip to main content

TanStack Start Consent API

Callbacks

Pass callbacks to ConsentRoot

Keep the callbacks in their own module:

src/consent-callbacks.ts
import type { ConsentProviderCallbacks } from 'c15t/tanstack-start';

export const callbacks = {
	// Right before the page reloads because a save withdrew a permission.
	onBeforeConsentRevocationReload: ({ preferences }) => {
		console.info('Reloading with', preferences);
	},
	// The visitor clicked Accept all, Reject all or Save.
	onChoiceRecorded: ({ confirmed, snapshot }) => {
		console.info('Choice recorded', confirmed, snapshot.explicitChoice);
	},
	// A consent command failed, such as a save the backend rejected.
	onError: ({ error }) => {
		console.error('c15t', error);
	},
	// Permissions changed for any reason: a choice, an expired choice, a new
	// policy or a privacy signal.
	onPermissionsChanged: ({ previous, snapshot }) => {
		console.info('Permissions', previous, snapshot.effectivePermissions);
	},
} satisfies ConsentProviderCallbacks;

Import it in src/routes/__root.tsx and pass it under options on ConsentRoot:

src/routes/__root.tsx
<ConsentRoot
  state={consent}
  scripts={scripts}
  options={{ callbacks }}
>

The root route component renders on the server and again in the browser, and each side imports the module itself. Do not return callbacks from the root loader or a server function. Loader data is serialized for the browser, and functions cannot be serialized, so it should carry only the consent state.

ConsentRoot subscribes the callbacks after it mounts in the browser. Server rendering never calls them. The provider calls the latest callbacks object it received, so a new object on a later render takes effect without a remount.

Available callbacks

CallbackRuns whenReceives
onChoiceRecordedThe visitor clicks Accept All, Reject All or Save Settings, in the stock UI or through useSaveConsents().snapshot, the state after the choice; confirmed, the categories this action recorded; actionAt, the time as a millisecond timestamp
onPermissionsChangedAny effective permission changes value.snapshot, the state after the change; previous, the permissions before it
onErrorA consent command fails in the browser, such as a failed /init request or a save the backend rejects.error, a message string
onBeforeConsentRevocationReloadA save withdrew a granted category or vendor, right before c15t reloads the page.preferences, the permissions after the save

The type for the whole object is ConsentProviderCallbacks from c15t/tanstack-start. Callbacks run after c15t updates its state, so snapshot and hooks such as useConsent() already hold the new values.

resolveConsent on the server has no error callback. When the manifest request fails or runs out of timeoutMs, the loader returns the request-only state, and the browser resolves consent after hydration. A failure there reaches onError.

onChoiceRecorded or onPermissionsChanged

onChoiceRecorded runs only for a visitor's action. onPermissionsChanged runs whenever effective permissions change, whatever the cause:

EventonChoiceRecordedonPermissionsChanged
Visitor accepts, rejects or savesYesYes, if a permission changed
A policy resolves in the browser and allows categories before any choiceNoYes
Global Privacy Control turns off a grantNoYes
A recorded choice expiresNoYes
Visitor dismisses a noticeNoNo

Use onChoiceRecorded for consent analytics and audit trails, because it fires only when the visitor chose. Use onPermissionsChanged to start or stop your own code. Never treat it as proof that the visitor agreed to anything, and never save consent from it. How consent works explains the difference.

Dismissing a notice records no choice and changes no permission. Read it with useNoticeDismissal() instead. For consent inside one component, use the hooks, which re-render the component when consent changes.

Before a revocation reload

Removing a script tag cannot stop code that already ran, so c15t reloads the page after an accept, reject or save turns off a category or vendor that was granted. The reload waits for the save request to finish. Right before it, onBeforeConsentRevocationReload runs synchronously. Use it to call a vendor's shutdown API or flush queued events. Keep it short, because the reload does not wait for promises.

A choice that expires, a new policy and a privacy signal change permissions without a visitor action, so they do not reload the page.

To handle revocation yourself, set reloadOnConsentRevoked: false in options on ConsentRoot. Code the visitor turned off then keeps running until the next full page load, and onBeforeConsentRevocationReload never runs.

Verify

  1. Add the callbacks from this page, open the console and DevTools Network, clear site data and reload under a policy that shows a banner.
  2. Click Accept All. onChoiceRecorded and onPermissionsChanged both log, and the save request to your backend appears in Network.
  3. Reload. onChoiceRecorded does not log again.
  4. Open Privacy settings, turn off a category you allowed and save. onBeforeConsentRevocationReload logs, then the page reloads. After the reload, that category's vendor requests are absent from Network.