React 19 made actions ordinary; authority stayed hard
React 19’s stable Actions and form primitives improved V0M3’s pending and optimistic interfaces, while document authority, idempotency, policy, and recovery remained domain work.
React 19 reached stable release on December 5 with a vocabulary I had already been testing since the release candidate: Actions, form actions, useActionState, useOptimistic, and useFormStatus.
They made an important class of interface work feel ordinary. An asynchronous transition could carry pending state. A form could invoke an action directly. Optimistic presentation could live close to the interaction that needed it.
None of those primitives could decide whether a generated patch was allowed to become a document revision.
The release improved how V0M3 presented a command. It did not supply the command's authority.
The stable release changed my adoption posture
In April I had evaluated the release candidate without making a stable application depend on unfinished assumptions. By December the public API was stable enough to replace several hand-built form and transition conventions in V0M3's current branch.
I still adopted one workflow at a time. The proposal-comment form was low consequence and useful for learning pending, validation, and reset behavior. Patch acceptance was higher consequence, so it came later with existing command fixtures unchanged.
The domain service did not know which React primitive invoked it. That separation let the UI migrate without rewriting document rules at the same time.
Stable framework status reduced one kind of risk. It did not justify a flag-day conversion.
An Action was an interaction boundary
In V0M3, an Action adapted form or transition input into a server-side application command. It parsed the small payload, established the current requester, called a domain service, and returned a typed result the interface could render.
The domain command carried the real conditions:
AcceptPatch {
documentId
baseRevision
proposalRevision
patchDigest
approvalReceipt
requestId
}Calling the function inside an Action did not make those fields redundant. It made their path from interface to service cleaner.
This naming distinction mattered because “Action” could otherwise expand until it meant button handler, business decision, mutation, external effect, and success state at once.
The server re-established identity
A client could submit document and proposal identifiers. It could not assert that the current user owned them or that the proposal belonged to the document.
The Action resolved authentication and workspace membership on the server. The service loaded document, task, proposal, policy, and approval state through bounded repositories. It checked that the exact requester could perform the command against the current revision.
Hidden form fields were transport convenience, not trusted facts. A modified request with another document ID failed authorization before any patch content was read into the command path.
The framework moved code to a server boundary. Application code still had to make that boundary meaningful.
Form data arrived as strings and files. I used a narrow parser to reject missing fields, malformed identifiers, unexpected collections, and oversized text. The result was a syntactically valid command candidate.
Domain validation then checked relationships and current state: base revision still current, proposal fully generated, operations valid, evidence requirements satisfied, approval exact and unused, policy revision current, and no conflicting command already committed.
The Action returned typed invalid, forbidden, conflict, expired, unavailable, and accepted outcomes. It did not convert every expected result into an exception or one red error string.
React made submission convenient. It did not collapse syntax and meaning into the same check.
useActionState carried a result, not the document
useActionState let the acceptance form render the result of its Action alongside pending state. That was a good home for field feedback, stale-review notices, and the receipt of a successful command.
It was not the canonical document store. The authoritative document revision remained on the server and was reloaded or reconciled from the accepted receipt.
I kept returned state small and serializable: outcome kind, request identity, current or new revision, safe validation issues, and next permitted actions. It did not echo the whole document or retain sensitive source text in component state.
If the component unmounted, the command result still existed in durable receipts. Interface state could disappear without making the operation unknowable.
Pending described one submission boundary
The pending value answered whether this Action submission was in progress from the interface's perspective. It did not prove that no background generation, evidence retrieval, export delivery, or competing edit was running.
V0M3 labelled the button “Accepting patch” while the command was unresolved. Elsewhere, task status and document status came from their own records.
The page did not disable unrelated reading or manual editing globally because one form was pending. It disabled or guarded actions that would invalidate the exact review, and a concurrent edit intentionally caused the acceptance precondition to fail.
One Boolean remained useful because I refused to make it describe the whole product.
Form status improved local composition
useFormStatus allowed a submit control nested within the form to observe submission status without plumbing props through every review component. It made the button, progress text, and accessible live message easier to keep consistent.
The hook's scope followed the parent form. That encouraged a healthy component boundary: one form represented one explicit command. A page with Prepare approval, Accept patch, Reject proposal, and Add note used separate forms or interactions because those commands had different consequences.
I avoided one giant form whose pending state made unrelated controls appear coupled. Component convenience followed domain separation rather than deciding it.
The status API reduced wiring. The words shown to the author still came from precise product states.
useOptimistic was tempting for acceptance: show the proposed paragraph as canonical immediately, then reconcile later. I rejected that use.
Acceptance could conflict, expire, or be forbidden. Presenting the generated patch inside the canonical document before commitment would make a proposal look authoritative during the very interval when authority was unresolved.
V0M3 used optimism for review annotations, selection affordances, and low-consequence local presentation. A newly added comment could appear pending with a clear status and disappear or retry safely on failure. A selected operation could respond instantly before server validation.
The canonical document changed only after a durable acceptance receipt named the new revision.
Optimistic UI was a design decision about temporary belief, not a default reward for using the hook.
Uncontrolled reset behavior needed explicit intent
Successful form Actions could reset uncontrolled form fields. That was welcome for a short comment and alarming for a complex review form whose input represented careful selection.
I tested success, validation failure, conflict, network interruption, and duplicate submission. Selection state lived in explicit review state where it needed to survive. A failed or stale acceptance preserved the inspected patch and annotations. A confirmed comment form could clear after the receipt returned.
No workflow relied on accidental DOM persistence as durable draft storage. Important notes were saved deliberately or warned before navigation.
Framework defaults were helpful only after the product defined what should be forgotten.
Error boundaries were not expected outcomes
Unexpected exceptions still belonged in operational error handling and error boundaries. They indicated a violated assumption, unavailable dependency without a mapped result, or bug the application could not explain safely.
Expected document outcomes returned data. A revision conflict showed the current revision and a Rebase review action. An expired receipt kept the diff and offered Prepare again. A forbidden result explained the relevant capability without exposing broader permissions. A duplicate request returned the existing receipt.
This made recovery specific and kept error telemetry meaningful. If every invalid form became an exception, real failures would drown in normal product behavior.
Actions made async code direct. The state machine still needed a complete result vocabulary.
Form-based actions invited a good question: what remained usable when client JavaScript was delayed or unavailable?
V0M3's server response could render validation and conflict states from submitted data. Core comment and review-note forms retained semantic labels and buttons. Complex operation selection still benefited from client behavior, but its server-rendered page displayed the exact proposed patch and did not show a false active control when the enhancement was absent.
I tested keyboard submission, repeat activation, reload after success, and browser navigation. The URL for an accepted revision reconstructed the receipt-backed result.
Progressive enhancement was not automatic because a <form> appeared. The entire response and navigation path had to remain coherent.
A person could double-click, a browser could repeat a request, or a response could disappear after the server committed. Disabling a pending button reduced one path and did not establish exactly-once behavior.
Every consequential command carried a stable request identity. The document service stored it with the resulting receipt under a uniqueness constraint. Repeating the same command returned the same accepted revision. Reusing the identity with different normalized arguments failed.
The Action adapter created or preserved the identity across retry. The UI could safely offer Check status after an interrupted response.
Idempotency remained a domain and storage property. Pending presentation made its common case less noisy.
Revalidation did not prove commitment
After acceptance, V0M3 refreshed views that depended on the document revision. Revalidation made caches and server-rendered pages current. It was not the commit itself.
The service first wrote document revision, operation lineage, approval consumption, and receipt atomically within its database boundary. Only then could the adapter invalidate or refresh projections. If refresh failed, the command remained accepted and a reload reconstructed it.
Conversely, refreshing a route before durable success could never be displayed as proof that the patch landed. The receipt and new revision established that fact.
Cache ergonomics followed state change rather than standing in for it.
External effects stayed outside the document transaction
An export Action could prepare or deliver an artifact. The document transaction could not include a remote destination, so V0M3 reused Z29C's intent, attempt, and receipt model.
The Action recorded one export intent and returned its current state. A worker or bounded adapter performed delivery with an idempotency key where supported. Unknown outcomes paused for query or review. A second submit did not create a second export consequence.
This meant some Actions completed before the larger workflow completed. The interface said prepared, queued, delivered, failed, or outcome unknown rather than displaying one success state for all of them.
Async mutation syntax did not make distributed effects transactional.
Security review included serialized state
Moving an Action behind a form did not excuse inspection of what crossed the server-client boundary. I checked returned results, rendered props, hidden fields, logs, stack traces, and development diagnostics for document content, source excerpts, credentials, or unrestricted identifiers.
The client received only information needed to render the review. Server dependencies and credentials were constructed inside the trusted module. Error messages were mapped to safe product explanations while detailed evidence stayed in protected logs and traces.
I also checked closure capture and build output when framework code transformed server functions. A convenient function reference should not accidentally serialize a capability.
The correct boundary was verified, not inferred from file location.
Pending text was exposed in an appropriate live region without repeatedly announcing token-level progress. Buttons preserved their accessible names while adding status. Validation issues linked to the relevant selection or field. Conflict results moved focus to a clear heading rather than silently replacing the page.
Disabling controls was selective. The author could still copy the patch, inspect evidence, or cancel a local selection while a comment submitted. When a command truly could not be repeated, the interface explained why instead of only lowering opacity.
React's form context helped components coordinate. Accessible consequence still depended on product copy, focus, semantics, and recovery.
The more asynchronous the page became, the more valuable those explicit states were.
Actions integrated with React's transition model, which helped keep urgent input responsive while asynchronous work progressed. I avoided reading that scheduling concept as a workflow priority.
A low-urgency render update could represent a high-consequence command. A visually urgent keystroke could have no durable effect at all. V0M3's job priority, deadline, tool budget, and approval policy lived in its own records.
The adapter could start an Action within the appropriate UI transition and display interruption gracefully. It could not let rendering priority reorder accepted document operations or external effects. Sequence came from command and workflow state.
Sharing the word priority across layers would have made a convenient API into a misleading domain metaphor.
Server Components did not become a homemade framework
React 19 also stabilized the component-facing Server Components model while leaving framework and bundler implementation details to the surrounding ecosystem. V0M3 consumed those capabilities through its framework instead of building a private transport around unstable internals.
Server-rendered document and review views reduced client data loading and kept sensitive repository access on the server. Interactive selection, annotations, and local optimistic affordances remained client components at clear boundaries.
I did not treat “server” in a component name as authorization. Data access still checked requester and workspace, serialized props stayed minimal, and every command revalidated current state.
The architecture benefited from server-first composition without confusing rendering placement with a security policy.
An author could load a review page from one deployment and submit after a new version reached the server. The client might carry an older form shape or patch normalization version.
V0M3 included command and review-format versions in the prepared receipt. The Action parser accepted only supported shapes and the service evaluated the pinned patch through compatible logic. An incompatible old page received a refresh-required result while preserving its proposal and notes.
Running tasks kept their domain definition revision. New UI code did not reinterpret an old approval automatically. Rolling deployment tests submitted across adjacent versions and verified that duplicate request identities still converged.
Stable React APIs made deployments easier to reason about; application-version skew still required an explicit contract.
Observability named every layer
One span called server_action could show latency and hide meaning. V0M3 traces distinguished form received, authentication established, command parsed, policy evaluated, receipt consumed, document committed, projection refreshed, and response delivered.
The trace linked React request identity to the stable product request without making framework identifiers durable business keys. Metrics separated validation conflicts from unexpected exceptions and slow domain work from slow rendering.
This mattered when the interface remained pending after the document had already committed. The trace showed whether response delivery, cache refresh, or client navigation lagged, and the receipt let the page recover.
Good ergonomics did not remove layers. Good observability kept them from becoming one mysterious wait.
The tests kept the domain adapter replaceable
Unit tests exercised parsing and result mapping. Integration tests submitted forms through the server boundary with valid, invalid, forbidden, conflicted, expired, and duplicate commands. Domain fixtures ran independently of React.
One architectural test invoked the same acceptance service through a small command-line adapter. It produced the same receipt and conflict rules. This prevented useActionState return shapes from leaking into the document state machine.
Browser tests checked pending presentation and focus without treating DOM text as the only proof of commitment. They followed the receipt to the server-rendered document revision.
If React changed again, V0M3 could replace the interaction adapter without rediscovering what acceptance meant.
Stable did not mean finished judgment
React 19 made the primitives official. My choices around them remained subject to revision. I could still overuse optimistic state, create forms at the wrong boundary, or return too much server data because the path felt convenient.
The release removed uncertainty about whether these public APIs belonged in stable React. It did not remove the need to measure interaction quality, inspect security boundaries, or model failure.
That distinction helped me enjoy the release without assigning it work it never claimed to do.
Framework progress is most useful when it lets application concepts become clearer.
The hard part stayed product-shaped
After the migration, V0M3 had less pending-state boilerplate and more consistent form composition. Comments, proposal decisions, and approval preparation read naturally in the component tree. The framework deserved credit for that improvement.
The difficult guarantees remained where they belonged: exact target revisions, normalized patches, source lineage, current authorization, approval receipts, idempotency, durable commitment, unknown external outcomes, and humane recovery.
Those guarantees were not evidence that React Actions had failed to abstract enough. They were the substance of this particular product.
React 19 made actions ordinary in the interface. V0M3 still had to decide which action was being requested, who could request it, what state it targeted, what consequence it caused, and how the truth could be recovered afterward.
Ergonomics shortened the path to the boundary. Authority was the boundary.