Subscribe to router’s global blocking state
Hey 👋
Is there a way to subscribe to TanStack Router’s global blocking state?
Use-case
Root-level component that should pop up whenever any navigation is blocked anywhere in the app. Something like:
From the docs, useBlocker() seems scoped to its consuming route. What I’d like instead is a hook/event that tells a top-level component: “the router is blocking right now.”
Couldn’t find any 'blocking' property or similar state in the router history or internals
Seems like the history object only holds a 'block' method, but doesnt holds the blocking state
https://github.com/TanStack/router/blob/192ee6246cdbd3a38e2fd8969a7522bbda64c0d3/packages/history/src/index.ts#L35C1-L36C1
Does such a hook exist, or any recommended way to listen to this state?
Thanks! 🙏
GitHub
router/packages/history/src/index.ts at 192ee6246cdbd3a38e2fd8969a7...
🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering. - TanStack/router
27 Replies
optimistic-gold•3mo ago
there is no such thing as "blocking state"
either the history accepts a navigation or it blocks it
so the only you could get is a "blocked event"
jolly-crimsonOP•3mo ago
Hi @Manuel Schiller thanks for answering,
Is there a way to listen to the "blocked event" globally somehow ?
And I guess there should be somekind of "discard" / "reset" event also ?
optimistic-gold•3mo ago
we don't have a way to listen to the blocked event, but it's probably a rather small change in the history implementation
are you interested in contributing?
jolly-crimsonOP•3mo ago
Yes. Took a quick look at the history implementation already—just wanted to be sure it wasn’t there.
Happy to open a PR. Any pointers on how the implementation should look like?
optimistic-gold•3mo ago
not right now, maybe just experiment a bit and create a draft PR so we have something to discuss
jolly-crimsonOP•3mo ago
Sound good,
I'll start playing with it and open a draft
will send an update soon
Thanks!
exotic-emerald•3mo ago
I brought this up a while back, that it would be nice to have a global hook that would listen or know if route is blocked
Good to know others might find it useful 😅
jolly-crimsonOP•3mo ago
Hi
Added a PR with some initial implementation
https://github.com/TanStack/router/pull/4398
GitHub
feat(router-core,history): Global blocking status & proceed/reset a...
This PR introduces a global “blocker” state in and the callbacks needed to continue (proceed) or cancel (reset) a blocked navigation globally, to be consumed directly from the router state.
This en...
optimistic-gold•3mo ago
thanks for that, will take some time to dig into this since we are currently fixing regressions of our latest releases.
jolly-crimsonOP•2mo ago
Hi @Manuel Schiller
Any chance to get a review on this ? thanks
Any chance to get a review on this ? thanks
optimistic-gold•2mo ago
cc @ferretwithabéret can you please have a look at this PR?
absent-sapphire•2mo ago
Looked a bit through it, afaik, we can have multiple blockers active at a time. But this PR sets a new
blocker
property in the __store
which likely only holds the last blocker that notified with the BLOCK
action type
The use of async generators does confuse me, not that it is a wrong approach, rather because I did not work at all with them in the pastjolly-crimsonOP•2mo ago
Hi @ferretwithabéret
Thanks for your feedback!
Regarding the async generators, I agree that it might not seem the straightforward approach. I'll try to find a different way for this that might be simpler, I just found it to be effective for this use case.
Regarding the multiple blockers, that might be an issue. I'm trying to think of a use case where there's a need for multiple blockers.
I could either hold each "blocker" state inside the __state in a list. Or we could turn the global "proceed"/"dismiss" function to a "proccedAll"/"dismissAll" functions
absent-sapphire•2mo ago
It's not necessary to change the async generators, I mentioned that because I never worked with them myself and maybe I misunderstood the code
jolly-crimsonOP•2mo ago
Or if you think there's a better way to achieve some kind of global blocker that's different altogether
Yes I wasnt sure about them my self, but they provide a solution for this case where theres need to split the promise to 2 steps
absent-sapphire•2mo ago
simplest I can think, but bug prone and maybe confusing is having an integer that counts how many blockers are blocking
when that int is 0, no blocking is happening, when it is != 0, it is blocked
jolly-crimsonOP•2mo ago
That could work. I think mostly the problem is not with the "blocking" state, but more with the dismiss/proceed callbacks.
Im thinking what would be the best way to handle those globally when there's multiple blockers
absent-sapphire•2mo ago
Let me have another look at the PR and at the current code
jolly-crimsonOP•2mo ago
Sure
I've created a small example in
examples/react/global-blocking-state
to play with.
Might be good to look there to see the desired feature from the consumer side
Thanks, appreciate you taking the timeabsent-sapphire•2mo ago
Also, another question, in your example, the developer is supposed to call
proceed
or reset
, otherwise the Promise will never resolve, right?
In some scenarios, people will use a blocker, but they will not provide a prompt to let the user decide what they want to do
they will just want to block navigation
so proceed/reset will never be calledjolly-crimsonOP•2mo ago
Yes
This shouldn't be a problem as long as the
blockerFn
that is passed to the history from the useBlocker
hook is resolved, which anyway must be resolved in the current implementaion as well.
So in order to achieve this use case where navigation was blocked without waiting for user promt the useBlocker
must be called like this:
the shouldBlockFn: () => true,
will cause the blocking state to resolve.
Also regarding multiple blocker, this should not be a problem as well, because the current implementation loops through each one already and await the shouldBlockFn
to resolve for each blocker.absent-sapphire•2mo ago
My main concern with multiple blockers is that
blocker
will only store the last blocker, and when you call proceed
it would go into the idle
status
while there are other blockers still blockingjolly-crimsonOP•2mo ago
What happens in the case of multiple blockers is this:
1. Blocker 1 blocks navigation
2. BLOCK event is fired from history
3. Router state is updated with blocker 1's proceed / dismiss callbacks
4. Blocker 1 is resolved (when its corresponding blockerFn is resolved)
5. BLOCK_DISMISSED event is fired
6. History continues to the next blocker in the loop
Steps repeat for blocker 2 ... blocker n
So if I’m not mistaken, the state will always point to the currently active blocker at that point in time.
You can see that happening here:
https://github.com/nizans/router/blob/05b38eb2bf25a57acebf240b0cbb6a2748fa69c8/packages/history/src/index.ts#L152
The current (non-global) implementation already works the same way — just without steps 2, 3, and 5.
That’s exactly where the async generators come in handy: they allow us to hook into that intermediate “blocked” state before the final resolution.
absent-sapphire•2mo ago
I see, that makes sense
With multiple blockers you'd need to call
proceed
for each one individually though, right?
I guess, this could be fine for now, but should be reworked in the future, as I'd expect a global proceed
function to ignore all blockers
or maybe we can have another function to "proceedAll
"
proceedAll
could just retry the navigation with ignoreBlocker: true
jolly-crimsonOP•2mo ago
Yeah
I think proceed / dismiss is be a bit confusing here.
In reality, they just represent resolving the blockerFn — where proceed() resolves it with true, and dismiss() resolves it with false (or maybe its the other way around - cant remember).
So with multiple blockers, each blockerFn still needs to be resolved individually — and I believe that’s how the current implementation behaves as well.
This change doesn’t alter that logic, it just adds the ability to resolve each blockerFn also from the global state, and exposes the blocking status globally too.
Now that we have access to all the resolvers, a proceedAll / dismissAll is definitely doable — I can add that, makes a lot of sense.
Using
Just pushed a commit with the
ignoreBlocker: true
might help for skipping blockers, but I’m not sure how that would apply to a dismissAll scenario - in this case we probably want to stop the navigation and stop running the rest of the blockers blockerFn
I guess?.
Oh on second look
If one blocker blocks the navigation, we already skip checking for other blockers ( makes sense )
So there only a need for a proceedAll - which just ignore's the rest of the blocker like you mentioned
I'll try to add it to the PR using a navigation retry with "ignoreBlocker" true
Hi @ferretwithabéretJust pushed a commit with the
proceedAll
functionality we discussed.
Also added an example to the example page.
Feel free to take a look whenever you get the chanceoptimistic-gold•2mo ago
could you please convert that example to an e2e test?
aside from that, i am still not convinced about moving the blocker state into router
especially storing the functions in router state feels weird
jolly-crimsonOP•2mo ago
Yes, sounds good.
Regarding the router state — I get why it feels off.
Now that history emits the blocking events, that should be enough to support the feature.
I originally added it for convenience, but I can remove it and maybe create a wrapper hook that listens to those events and exposes the blocking state — independent of the router itself.
I’ll add that to the PR soon and update.
hi @Manuel Schiller
Added a commit with the change and e2e tests Feel free to take a look whenever
Added a commit with the change and e2e tests Feel free to take a look whenever