Skip to content

React: useRejections

useRejections reads the client-local journal of every write the server refused, newest first, and returns the action that acknowledges one. The engine's rejection events are fire and forget; this journal survives reloads until each entry is dismissed, so a component can explain a write that vanished an hour ago.

Examples#

Basic#

// src/components/rejection-count.tsx
import { useRejections } from 'kizunasync/react'

export function RejectionCount() {
  const { rejections } = useRejections()

  return <span>{rejections.length} writes refused</span>
}

Render the journal#

// src/components/rejection-list.tsx
import { useRejections } from 'kizunasync/react'

export function RejectionList() {
  const { rejections, error, isLoading, dismiss } = useRejections()

  if (isLoading) return null
  if (error !== null) return <p>The journal could not be read: {error.message}</p>

  return (
    <ul>
      {rejections.map((entry) => (
        <li key={entry.mutationId}>
          {entry.table} {entry.kind}: {entry.reason}
          <button onClick={() => dismiss(entry.mutationId)}>Dismiss</button>
        </li>
      ))}
    </ul>
  )
}

Parameters#

NameTypeRequiredDescription
optsIRejectionsOptionNoClient override plus the dismissed filter. Default: the context client and live entries only.
opts.clientIKizunaSyncNoUsed instead of the KizunaSyncProvider client, resolved through useKizunaSync. Default: the context client.
opts.includeDismissedbooleanNoWhen true, entries already acknowledged stay in the list, which is what a history view wants. Changing it re-reads the journal. Default: false.

Returns#

IRejectionsResult.

NameTypeRequiredDescription
rejectionsTRejectionRecord[]—The journal rows the last read returned, newest first. [] before the first read resolves and after a failed one.
errorError | null—The failure the last read raised, wrapped in an Error when it was not one. Cleared by the next successful read.
isLoadingboolean—true until the first read settles, then false for the life of the subscription.
dismiss(mutationId: TUuid) => Promise<void>—Calls dismissRejection for one entry and then awaits a fresh read, so the list already reflects the change when the promise settles.

Fields on one TRejectionRecord, the same record List rejections returns from the client:

NameTypeRequiredDescription
mutationIdTUuid—The identity of the refused write, and the argument dismiss takes.
tablestring—The table the write targeted.
pkTRowKey—The primary key of the row the write targeted.
kind'REJECTED' | 'SUPERSEDED' | 'DEAD_LETTER' | 'BATCH_ABORTED'—Which path refused the write: a per-mutation verdict, a newer write that won, an exhausted retry budget, or an atomic batch the server refused.
reasonstring—The verdict reason as the server worded it, one of CONSTRAINT, COLUMN_DENIED, DELETE_WINS, PRECONDITION, RLS_DENIED, and SUPERSEDED for the first three kinds. A DEAD_LETTER entry carries PERMANENT_TRANSPORT, except an atomic batch the server dead-letters at once for its size (KZP02), which carries the server's own message instead.
changedColumnsstring[]—The columns the refused mutation carried, which is what a component highlights when it offers to re-apply the edit.
serverRowTColumnValues | null—The authoritative row the verdict carried, or null when it carried none. This is the state the local row was reverted to.
atnumber—Epoch milliseconds when the entry was journalled.
dismissedboolean—Whether this entry has been acknowledged. Only ever true in a list read with includeDismissed.

Errors#

A failed read lands in error rather than being swallowed, because a component that cannot read this journal has to be able to say so instead of rendering an empty list as though nothing had gone wrong.

dismiss rejects when dismissRejection fails, so a button handler can catch it. A dismiss whose follow-up read fails resolves regardless, with that failure in error.

A component that renders outside KizunaSyncProvider and passes no { client } override throws from useKizunaSync.

Notes#

The list re-reads on MUTATION_REJECTED, BATCH_ABORTED, and DEAD_LETTER, and on nothing else. Those three are the only events that add or change a journal row. A SUPERSEDED entry needs no event of its own, because it rides MUTATION_REJECTED. Narrowing the subscription to those three therefore misses nothing, and it keeps a pull of a thousand rows from re-reading the journal.

Dismissing is an acknowledgement, not a repair. The local row was already reverted to the server state when the verdict arrived, so re-applying the edit means writing it again through useMutation. Read the journal in the app shows both halves against a policy that refuses the write.

An RLS_DENIED reason means Row Level Security refused the write on the server. Kizuna adds the timing: the policy is judged when the queued write reaches the server rather than when the component renders, so this journal is where the verdict surfaces.

Found a problem? Edit this page on GitHub