feat: Update `useBlocker` hook to provide current and next location when conditionally blocking
Currently, when using the useBlocker hook there is no way to ascertain where the navigation being blocked is headed.
For my usage of blocking, this is necessary to allow navigation within a selected bunch of nested routes, but not allowing the user to exit the base route. Thought this could be a useful feature to have.
we wanted to follow react-router's API for this:
https://reactrouter.com/en/main/hooks/use-blocker
can you adjust your PR to match it?
also, the location should be typesafe instead of just a string.
additionally, we would also need tests for useBlocker
we wanted to follow react-router's API for this:
https://reactrouter.com/en/main/hooks/use-blocker
can you adjust your PR to match it?
also, the location should be typesafe instead of just a
string.additionally, we would also need tests for
useBlocker
Yes sounds good, I will update the PR as soon as possible
This implementation should now be providing the blockerFn inuseBlocker with a similar API to the hook in react-router. This also makes the blocker catch backwards navigation using the browser back buttons, which is very beneficial for our use case.
Things left to do:
- [x] Tests
- [ ] Update docs/examples?
I am not quite sure how to do with the old blockerFn since it now behaves in a completely different way now. So any hints from someone on this would be appreciated.
Would also like to give a quick pitch for a different interface that could be used for useBlocker which I have implemented in our codebase with these changes:
import React from 'react'
import type { BlockerFn } from '../../lib/router/qudak-router'
import { BlockerFn, useRouter } from '@tanstack/react-router'
type BlockerOpts = {
blockerFn: BlockerFn
disabled?: boolean
}
export function useBlocker({ blockerFn, disabled = false }: BlockerOpts) {
const { history } = useRouter()
React.useEffect(() => {
return disabled ? undefined : history.block(blockerFn as BlockerFn)
}, [blockerFn, disabled, history])
}
Which gives very clean implementations for custom UIs using hooks and could look something like this:
useBlocker({
blockerFn: () => {
if (blockerDisabled && blockerDisabled.current) {
blockerDisabled.current = false
return false
}
if (!blockerDisabled && _blockerDisabled.current) {
_blockerDisabled.current = false
return false
}
const shouldBlock = props.blockerFn()
if (!shouldBlock) return false
const promise = new Promise<boolean>((resolve) => {
modals.open({
title: 'Are you sure you want to leave?',
children: (
<SaveBlocker
onSave={onSave}
onDiscard={onDiscard}
close={() => {
modals.closeAll()
resolve(false)
}}
confirm={() => {
modals.closeAll()
resolve(true)
}}
reject={() => {
modals.closeAll()
resolve(true)
}}
/>
),
onClose: () => resolve(false)
})
})
return promise
},
disabled
})
Any thoughts on this?
This would be a breaking change to useBlocker, right? then we cannot do it (now, only possible in v2).
While this might be out of scope for this PR. I think we can likely do much better from a type safety point of view for useBlocker. Correct me if I'm wrong but useBlocker blocks a navigation and we usually describe navigations with from and to.
I was imagining a more type safe hook.
useBlocker({ from: Route.fullPath, to: '..', blockerFn: ({ fromMatch, toMatch }) => // have I saved? })
fromwould describe a route matching the current location and would narrow thefromMatchpassed toblockerFnfor the matching route. This will be optional,fromMatchwill be loose if not providedtowould describe a route matching the destination location and would narrow thetoMatchpassed toblockerFnfor the matching route. This will be optional,toMatchwill be loose if not providedblockerFndetermines if the navigation should continue or block.fromMatch/toMatchis a match like fromuseMatchwhich will provide the usualparams,search,context, etc
When from or to is provided the blockerFn is only called when navigating to the location described by these two properties. If they are not provided it will always call blockerFn for any navigation
we should make this as typesafe as possible, as per @chorobin's comment. @tomrehnstrom can you tackle this? if you need support let us know
@tomrehnstrom, are you planning to continue working on this PR?
Does anyone know a quick and dirty workaround I can apply in the meanwhile?
@tomrehnstrom, are you planning to continue working on this PR?
Does anyone know a quick and dirty workaround I can apply in the meanwhile?
Yes, but have been quite busy elsewhere recently. The way i have solved this quickly in our codebase is just to copy the history package index.ts file and make the changes required to make it work, and then use it as a custom history:
const history = createCustomBrowserHistory()
export const router = createRouter({
routeTree,
defaultPendingComponent: FullWidthLoader,
defaultErrorComponent: RouterSuspenseErrorFallback,
defaultNotFoundComponent: Error404,
context: {
auth: undefined!,
queryClient
},
defaultPreload: 'intent',
// Since we're using React Query, we don't want loader calls to ever be stale
// This will ensure that the loader is always called when the route is preloaded or visited
defaultPreloadStaleTime: 0,
history
})
With the changes in my first commits to this pr
Then written a custom blocker hook like this:
type BlockerOpts = {
blockerFn: CustomBlockerFn
disabled?: boolean
}
export function useCustomBlocker({ blockerFn, disabled = false }: BlockerOpts) {
const { history } = useRouter()
React.useEffect(() => {
return disabled ? undefined : history.block(blockerFn as BlockerFn)
}, [blockerFn, disabled, history])
}
Thanks @tomrehnstrom, I needed this urgently and managed to make do with the suggested temporary solution for now.
☁️ Nx Cloud Report
CI is running/has finished running commands for commit 2ee506f9cf2b18ee35ad0c225095c5e43a03d4e2. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.
📂 See all runs for this CI Pipeline Execution
| 🟥 Failed Commands |
|---|
nx affected --targets=test:eslint,test:unit,test:e2e,test:types,test:build,build --parallel=3 |
✅ Successfully ran 1 target
Sent with 💌 from NxCloud.
More templates
- tanstack-router-react-example-authenticated-routes
- tanstack-router-react-example-basic
- tanstack-router-react-example-basic-default-search-params
- tanstack-router-react-example-basic-file-based
- tanstack-router-react-example-basic-file-based-codesplitting
- tanstack-router-react-example-react-query
- tanstack-router-react-example-basic-react-query-file-based
- tanstack-router-react-example-basic-ssr-file-based
- tanstack-router-react-example-basic-ssr-streaming-file-based
- tanstack-router-react-example-basic-virtual-file-based
- tanstack-router-react-example-basic-virtual-inside-file-based
- tanstack-router-react-example-deferred-data
- tanstack-router-react-example-kitchen-sink
- tanstack-router-react-example-kitchen-sink-file-based
- tanstack-router-react-example-kitchen-sink-react-query
- tanstack-router-react-example-kitchen-sink-react-query-file-based
- tanstack-router-react-example-large-file-based
- tanstack-router-react-example-location-masking
- tanstack-router-react-example-navigation-blocking
- tanstack-router-react-example-quickstart
- tanstack-router-react-example-quickstart-esbuild-file-based
- tanstack-router-react-example-quickstart-file-based
- tanstack-router-react-example-quickstart-rspack-file-based
- tanstack-router-react-example-quickstart-webpack-file-based
- router-monorepo-react-query
- router-mono-simple
- tanstack-router-react-example-scroll-restoration
- tanstack-search-validator-adapters
- tanstack-start-example-basic
- tanstack-start-example-basic-auth
- tanstack-start-example-basic-react-query
- tanstack-start-example-basic-rsc
- tanstack-start-example-clerk-basic
- tanstack-start-example-convex-trellaux
- tanstack-start-example-counter
- tanstack-start-example-large
- tanstack-start-example-supabase-basic
- tanstack-start-example-trellaux
- tanstack-router-react-example-with-framer-motion
- tanstack-router-react-example-with-trpc
- tanstack-router-react-example-with-trpc-react-query
@tanstack/arktype-adapter
npm i https://pkg.pr.new/@tanstack/arktype-adapter@1790
@tanstack/create-router
npm i https://pkg.pr.new/@tanstack/create-router@1790
@tanstack/eslint-plugin-router
npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@1790
@tanstack/history
npm i https://pkg.pr.new/@tanstack/history@1790
@tanstack/react-cross-context
npm i https://pkg.pr.new/@tanstack/react-cross-context@1790
@tanstack/react-router
npm i https://pkg.pr.new/@tanstack/react-router@1790
@tanstack/react-router-with-query
npm i https://pkg.pr.new/@tanstack/react-router-with-query@1790
@tanstack/router-cli
npm i https://pkg.pr.new/@tanstack/router-cli@1790
@tanstack/router-devtools
npm i https://pkg.pr.new/@tanstack/router-devtools@1790
@tanstack/router-generator
npm i https://pkg.pr.new/@tanstack/router-generator@1790
@tanstack/router-vite-plugin
npm i https://pkg.pr.new/@tanstack/router-vite-plugin@1790
@tanstack/router-plugin
npm i https://pkg.pr.new/@tanstack/router-plugin@1790
@tanstack/start
npm i https://pkg.pr.new/@tanstack/start@1790
@tanstack/start-vite-plugin
npm i https://pkg.pr.new/@tanstack/start-vite-plugin@1790
@tanstack/valibot-adapter
npm i https://pkg.pr.new/@tanstack/valibot-adapter@1790
@tanstack/virtual-file-routes
npm i https://pkg.pr.new/@tanstack/virtual-file-routes@1790
@tanstack/zod-adapter
npm i https://pkg.pr.new/@tanstack/zod-adapter@1790
commit: 2ee506f
For anyone stumbling across the PR and really wants this now, use my fork: https://github.com/tomrehnstrom/router/tree/better-blocker-router Until this can get merged :)
I need to have a closer look, what's the current status of this PR? anything obvious missing ?
The type safety and type narrowing described by @chorobin is not really implemented, but functionally of it should be complete, have been using this method in prod for quite a while and haven't had any issues so far. Maybe the old interface for useBlocker should still be supported and marked as deprecated though.
is your PR breaking the current useBlocker API or is it backwards compatible?
let's work on merging this. [] docs need updating
@tomrehnstrom thanks for the update still not sure if the API is backwards compatible. can you comment on that please? if it is not backwards compatible can you please fix this so we can release this in a non breaking manner
@tomrehnstrom thanks for the update still not sure if the API is backwards compatible. can you comment on that please? if it is not backwards compatible can you please fix this so we can release this in a non breaking manner
It should be backwards compatible now 👍 @schiller-manuel
@schiller-manuel @ThomasStock I truly appreciate your help here. Thank you so much!
Could this be modified to supply the navigation type to the notify function? I will try to make an addon for the history API after this PR merges which will keep track of the history state. The addon will help us hide back buttons when there is only 1 entry in the navigation state and, maybe, for implementing breadcrumbs. Also, can you explain why back, forward and go are of type "POP"? It would help me if they had their own navigation type. My rough ideea for the addon right now is that PUSH, POP, REPLACE would do their respective action (.push, .pop or .splice) on an array and increment/decrement a cursor variable, while BACK, FORWARD and GO would only increment/decrement the cursor variable.
Edit: Meant to say .splice not .replace
@ferretwithaberet Sorry for the long wait, would the changes in my latest commit accomplished this?
@ferretwithaberet Sorry for the long wait, would the changes in my latest commit accomplished this?
@tomrehnstrom I do not mind the wait, take your time, do not stress yourself. As to the implementation, I would need to get the index for the GO action, which actually is , as MDN calls it, a delta. I need it so I can know how much I would have to increase/decrease the cursor of the history state. Otherwise it looks good to me, great job.
I tried to test the code manually. When using the browser back/forward buttons, it seems that the popstate event gets triggered for both buttons, which in turn triggers a POP event regardless of navigation type.
Also, I could not find anything on the pushstate window event, does it even exist or am I missing something?
I will not be able to look much on this until monday night EEST, I will try to look for a solution then.
After this PR, I will be able to block based on the condition: currentUrl != nextUrl ? Thanks
@sagi-webiks
After this PR, I will be able to block based on the condition: currentUrl != nextUrl ? Thanks
yes!
I am working on making the API typesafe, stay tuned
Cool! I think using setup will be much more useful than having fromand to in the hook args
Anything here I can help with here @schiller-manuel?
@tomrehnstrom
we need
- type tests for useBlocker
- more unit tests in a full router setup to check that action, current and next are correctly passed into shouldBlockFn
- e2e tests for the legacy API and the new API
- update docs to showcase the new typesafe API that allows narrowing
I have also added a TODO whether we should return current and next in case the resolver is used. I thought about this when editing the example since we only know the status then but don't know current/next which might be necessary for UI?
Yes, I will jump on updating docs and supplying the current blocking state to the resolver.