Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-router): update useBlocker hook to provide current and next location when conditionally blocking #1790

Merged
merged 30 commits into from
Dec 15, 2024

Conversation

tomrehnstrom
Copy link
Contributor

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.

@schiller-manuel
Copy link
Contributor

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

@tomrehnstrom
Copy link
Contributor Author

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

@tomrehnstrom
Copy link
Contributor Author

tomrehnstrom commented Jun 20, 2024

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:

  • 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?

@tomrehnstrom tomrehnstrom changed the title feat: provide navigation task to useBlocker blockerFn feat: Update useBlocker hook to provide current and next location when conditionally blocking Jun 20, 2024
@schiller-manuel
Copy link
Contributor

This would be a breaking change to useBlocker, right? then we cannot do it (now, only possible in v2).

@chorobin
Copy link
Contributor

chorobin commented Jun 20, 2024

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? })
  • from would describe a route matching the current location and would narrow the fromMatch passed to blockerFn for the matching route. This will be optional, fromMatch will be loose if not provided
  • to would describe a route matching the destination location and would narrow the toMatch passed to blockerFn for the matching route. This will be optional, toMatch will be loose if not provided
  • blockerFn determines if the navigation should continue or block.
  • fromMatch/toMatch is a match like from useMatch which will provide the usual params, 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

@TanStack TanStack deleted a comment from BrendanC23 Jun 21, 2024
@schiller-manuel
Copy link
Contributor

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

@ThomasStock
Copy link

@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
Copy link
Contributor Author

tomrehnstrom commented Aug 5, 2024

@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])
}

@ThomasStock
Copy link

Thanks @tomrehnstrom, I needed this urgently and managed to make do with the suggested temporary solution for now.

Copy link

nx-cloud bot commented Aug 27, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 2ee506f. 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.

Copy link

pkg-pr-new bot commented Aug 27, 2024

Open in Stackblitz

More templates

@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

@tomrehnstrom
Copy link
Contributor Author

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 :)

@schiller-manuel
Copy link
Contributor

I need to have a closer look, what's the current status of this PR? anything obvious missing ?

@tomrehnstrom
Copy link
Contributor Author

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.

@schiller-manuel
Copy link
Contributor

schiller-manuel commented Oct 27, 2024

is your PR breaking the current useBlocker API or is it backwards compatible?

import type { BlockerFnArgs } from '@tanstack/history'
import type { UseBlockerOpts } from './useBlocker'

export function usePromiseBlocker({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have this? isn't useBlocker enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the skipResolver option to the normal useBlocker instead, look at the example in the docs i added to see why this pattern is nice for using modals when asking for discarding changes which is a common usecase.

@schiller-manuel
Copy link
Contributor

let's work on merging this.
[] docs need updating

@schiller-manuel
Copy link
Contributor

@sagi-webiks

After this PR, I will be able to block based on the condition: currentUrl != nextUrl ? Thanks

yes!

@SeanCassiere SeanCassiere changed the title feat: Update useBlocker hook to provide current and next location when conditionally blocking feat(react-router): update useBlocker hook to provide current and next location when conditionally blocking Dec 14, 2024
@schiller-manuel
Copy link
Contributor

I am working on making the API typesafe, stay tuned

@tomrehnstrom
Copy link
Contributor Author

Cool! I think using setup will be much more useful than having fromand to in the hook args

@tomrehnstrom
Copy link
Contributor Author

Anything here I can help with here @schiller-manuel?

@schiller-manuel
Copy link
Contributor

schiller-manuel commented Dec 14, 2024

@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?

@tomrehnstrom
Copy link
Contributor Author

Yes, I will jump on updating docs and supplying the current blocking state to the resolver.

@schiller-manuel
Copy link
Contributor

@tomrehnstrom can you ping me on discord please?

https://tlinz.com/discord

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

const { proceed, reset, status } = useBlocker({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { proceed, reset, status } = useBlocker({
useBlocker({

since withResolver is not true, this won't return the (unused) props

@@ -1,42 +1,425 @@
import React from 'react'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also need some tests that use the <Block> component

@schiller-manuel schiller-manuel merged commit f420670 into TanStack:main Dec 15, 2024
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants