T
TanStack3mo ago
jolly-crimson

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:
const Modal = () => {
const { status, dismiss, reset } = useBlockerStatus(); // imaginary hook

return status === 'blocking'
? <BlockingModal onDismiss={dismiss} onReset={reset} />
: null;
};
const Modal = () => {
const { status, dismiss, reset } = useBlockerStatus(); // imaginary hook

return status === 'blocking'
? <BlockingModal onDismiss={dismiss} onReset={reset} />
: null;
};
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
optimistic-gold3mo 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-crimson
jolly-crimsonOP3mo 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
optimistic-gold3mo 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-crimson
jolly-crimsonOP3mo 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
optimistic-gold3mo ago
not right now, maybe just experiment a bit and create a draft PR so we have something to discuss
jolly-crimson
jolly-crimsonOP3mo ago
Sound good, I'll start playing with it and open a draft will send an update soon Thanks!
exotic-emerald
exotic-emerald3mo 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-crimson
jolly-crimsonOP3mo 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
optimistic-gold3mo ago
thanks for that, will take some time to dig into this since we are currently fixing regressions of our latest releases.
jolly-crimson
jolly-crimsonOP2mo ago
Hi @Manuel Schiller
Any chance to get a review on this ? thanks
optimistic-gold
optimistic-gold2mo ago
cc @ferretwithabéret can you please have a look at this PR?
absent-sapphire
absent-sapphire2mo 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 past
jolly-crimson
jolly-crimsonOP2mo 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
absent-sapphire2mo 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-crimson
jolly-crimsonOP2mo 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
absent-sapphire2mo 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-crimson
jolly-crimsonOP2mo 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
absent-sapphire2mo ago
Let me have another look at the PR and at the current code
jolly-crimson
jolly-crimsonOP2mo 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 time
absent-sapphire
absent-sapphire2mo 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 called
jolly-crimson
jolly-crimsonOP2mo 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:
const { proceed, reset, status, next } = useBlocker({
shouldBlockFn: () => true,
withResolver: true,
})
const { proceed, reset, status, next } = useBlocker({
shouldBlockFn: () => true,
withResolver: true,
})
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
absent-sapphire2mo 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 blocking
jolly-crimson
jolly-crimsonOP2mo 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
absent-sapphire2mo 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-crimson
jolly-crimsonOP2mo 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 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éret
Just 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 chance
optimistic-gold
optimistic-gold2mo 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-crimson
jolly-crimsonOP2mo 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

Did you find this page helpful?