Three parts into this series, and if you'd asked me to define the word sitting underneath every single hook I'd taught you, I'm not sure I could ha...
For further actions, you may consider blocking this person and/or reporting abuse
So, "action" is the mother and the four "use" brothers. This is so complicated. Nice catch as always! π
Thanks! π That family analogy actually fits surprisingly well. The fun part is seeing how these pieces start making more sense once you see the Action model underneath them.
The note that
useOptimisticis the odd one out β a setter you call from inside an Action rather than a doorway into becoming one β is the kind of invariant that almost can't be expressed in a type signature, so it lives entirely in docs and reviewer intuition until someone calls it from the wrong scope and gets silent misbehavior. I've been running into the structural version of this problem in static analysis: ESLint rules that need to flag async patterns have to reason about call sites, not just function bodies, and that's exactly the same challenge you're describing β the function is neutral, the context is what promotes it. The four-doorway framing makes that explicit in a way I hadn't seen stated cleanly before. Wondering whether you think a lint rule could realistically enforce "don't call useOptimistic outside a Transition" or whether that's always going to be runtime territory.Thanks, Ofri. That's a really precise way to put it: the function is neutral, the context is what promotes it.
On the lint rule, I think you could catch some of the obvious misuse, but probably not enforce the invariant completely. A rule can reason about where the optimistic setter is called and flag cases where there's clearly no Transition context. The harder cases are when that context is indirect or the setter is passed through another function. At that point, the rule has to reason about the call chain and control flow rather than just the function body.
So I'd see linting as a useful guardrail, not the source of truth.
useOptimisticdoesn't create the Action context itself. Its setter assumes you're already inside one, which is a runtime relationship that isn't really expressible in the type signature.Great breakdown. The ordering section especially caught my attention.
Iβve been working on form/POST semantics in a deterministic application runtime, and it made me think about where the responsibility for an βActionβ should end.
pending, optimistic updates, and disabling submission are great UI concerns, but they donβt guarantee correctness once the request crosses the server boundary. Duplicate submissions, retries, concurrent requests, and out-of-order execution still need idempotency, transaction and concurrency semantics on the backend.
So Iβve started thinking of the form as just an adapter to an Action rather than the owner of it. The same Action should ideally be callable from a form, API, job, CLI, etc., while keeping the same validation and execution guarantees.
Your explanation of Actions as the shared mechanism underneath these APIs made that separation much clearer. Great article.
Thanks, Mustafa. I think that UI/server boundary is exactly the interesting part here.
Pending, optimistic updates, and disabling submission can make the UI behave correctly while an operation is in flight, but they don't make the operation itself safe against retries, duplicate submissions, or concurrent execution. Those guarantees have to come from the server-side operation and its contract.
And I like your point about treating the form as an adapter. Once you separate the React coordination from the underlying operation, the same operation can be reached from a form, API, job, or CLI without making those callers responsible for its correctness.
That's probably the boundary your comment makes especially clear: React can coordinate the UI around an Action, but it can't define what "correctly executing this operation twice" means. Whether duplicate execution is safe, rejected, or needs to be serialized is part of the server-side contract.
I like the distinction between the React Action and the underlying operation. βOperationβ is definitely the cleaner term here.
It keeps the boundary clear: React coordinates the interaction, while the operation owns correctness.
That separation becomes especially valuable once the same operation has multiple entry points. Thanks for sharpening that distinction.
Exactly. That distinction became much clearer to me once I separated the React-facing coordination from the operation itself. βOperationβ also feels like the better term because it doesn't tie the underlying work to any particular entry point. Glad that framing resonated.
"The same Action should ideally be callable from a form, API, job, CLI, etc" - I think that hits the nail on the head - it shouldn't matter what kind of "client" invokes the backend - the end result should be the same ...
Exactly, Leob. I think the important distinction is that the business operation should stay the same regardless of who invokes it, while the React-facing Action can be the adapter around that operation.
Even within React, the calling convention changes a bit. A form gives the Action
FormData, whileuseActionStateadds the previous state as the first argument. So I'd keep the actual business operation underneath that boundary and let the Action handle the React-specific coordination.That's probably the next layer I only hinted at in the post: the Action can be a thin adapter around the operation, rather than making the Action itself the business-logic boundary.
This was a really helpful way to tie everything together. I especially liked the explanation of why
useTransitiondoesnβt handle ordering the same wayuseActionStateand form Actions do. It makes React 19 Actions much easier to understand as one concept instead of a bunch of unrelated hooks.Thanks so much! π Iβm really glad that came through. The ordering distinction with
useTransitionwas important to include because the APIs can share the same Action model without providing the same guarantees. Once that clicks, the four stop looking like unrelated hooks and start feeling like one thing.Very well explained, and didactically (first lay out the problems and let the reader fully "grok" them - only later on the answers are laid out in full) this is probably the right way to structure it ...
Thanks, Leob! βΊοΈ That was exactly the bet with this one. I wanted readers to feel the problems first and only then put a name to the thing underneath them. By the time I finally got to βAction,β the difference between what
useActionStategives you and whatuseTransitiongives you should already feel pretty obvious. Glad that came through.Shubhra, I loved how you explained the four hooks as different answers to different problems. It made it much easier for me to understand why they are not really competing with each other π
Thank you so much, Hemapriya! π That was exactly what I wanted to make clear with this post.
useActionStateanduseTransition, for example, are solving different problems even though theyβre both using the same Action mechanism. Once you see that, the four APIs stop feeling like competing hooks and start making a lot more sense together.