import * as React from 'react' import { useRouter } from './useRouter' import type { BlockerFnArgs, HistoryAction, HistoryLocation, } from '@tanstack/history' import type { AnyRoute, AnyRouter, ParseRoute, RegisteredRouter, } from '@tanstack/router-core' interface ShouldBlockFnLocation< out TRouteId, out TFullPath, out TAllParams, out TFullSearchSchema, > { routeId: TRouteId fullPath: TFullPath pathname: string params: TAllParams search: TFullSearchSchema } type AnyShouldBlockFnLocation = ShouldBlockFnLocation type MakeShouldBlockFnLocationUnion< TRouter extends AnyRouter = RegisteredRouter, TRoute extends AnyRoute = ParseRoute, > = TRoute extends any ? ShouldBlockFnLocation< TRoute['id'], TRoute['fullPath'], TRoute['types']['allParams'], TRoute['types']['fullSearchSchema'] > : never type BlockerResolver = | { status: 'blocked' current: MakeShouldBlockFnLocationUnion next: MakeShouldBlockFnLocationUnion action: HistoryAction proceed: () => void reset: () => void } | { status: 'idle' current: undefined next: undefined action: undefined proceed: undefined reset: undefined } type ShouldBlockFnArgs = { current: MakeShouldBlockFnLocationUnion next: MakeShouldBlockFnLocationUnion action: HistoryAction } export type ShouldBlockFn = ( args: ShouldBlockFnArgs, ) => boolean | Promise export type UseBlockerOpts< TRouter extends AnyRouter = RegisteredRouter, TWithResolver extends boolean = boolean, > = { shouldBlockFn: ShouldBlockFn enableBeforeUnload?: boolean | (() => boolean) disabled?: boolean withResolver?: TWithResolver } type LegacyBlockerFn = () => Promise | any type LegacyBlockerOpts = { blockerFn?: LegacyBlockerFn condition?: boolean | any } function _resolveBlockerOpts( opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn, condition?: boolean | any, ): UseBlockerOpts { if (opts === undefined) { return { shouldBlockFn: () => true, withResolver: false, } } if ('shouldBlockFn' in opts) { return opts } if (typeof opts === 'function') { const shouldBlock = Boolean(condition ?? true) const _customBlockerFn = async () => { if (shouldBlock) return await opts() return false } return { shouldBlockFn: _customBlockerFn, enableBeforeUnload: shouldBlock, withResolver: false, } } const shouldBlock = Boolean(opts.condition ?? true) const fn = opts.blockerFn const _customBlockerFn = async () => { if (shouldBlock && fn !== undefined) { return await fn() } return shouldBlock } return { shouldBlockFn: _customBlockerFn, enableBeforeUnload: shouldBlock, withResolver: fn === undefined, } } export function useBlocker< TRouter extends AnyRouter = RegisteredRouter, TWithResolver extends boolean = false, >( opts: UseBlockerOpts, ): TWithResolver extends true ? BlockerResolver : void /** * @deprecated Use the shouldBlockFn property instead */ export function useBlocker(blockerFnOrOpts?: LegacyBlockerOpts): BlockerResolver /** * @deprecated Use the UseBlockerOpts object syntax instead */ export function useBlocker( blockerFn?: LegacyBlockerFn, condition?: boolean | any, ): BlockerResolver export function useBlocker( opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn, condition?: boolean | any, ): BlockerResolver | void { const { shouldBlockFn, enableBeforeUnload = true, disabled = false, withResolver = false, } = _resolveBlockerOpts(opts, condition) const router = useRouter() const { history } = router const [resolver, setResolver] = React.useState({ status: 'idle', current: undefined, next: undefined, action: undefined, proceed: undefined, reset: undefined, }) React.useEffect(() => { const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => { function getLocation( location: HistoryLocation, ): AnyShouldBlockFnLocation { const parsedLocation = router.parseLocation(undefined, location) const matchedRoutes = router.getMatchedRoutes(parsedLocation) if (matchedRoutes.foundRoute === undefined) { throw new Error(`No route found for location ${location.href}`) } return { routeId: matchedRoutes.foundRoute.id, fullPath: matchedRoutes.foundRoute.fullPath, pathname: parsedLocation.pathname, params: matchedRoutes.routeParams, search: parsedLocation.search, } } const current = getLocation(blockerFnArgs.currentLocation) const next = getLocation(blockerFnArgs.nextLocation) const shouldBlock = await shouldBlockFn({ action: blockerFnArgs.action, current, next, }) if (!withResolver) { return shouldBlock } if (!shouldBlock) { return false } const promise = new Promise((resolve) => { setResolver({ status: 'blocked', current, next, action: blockerFnArgs.action, proceed: () => resolve(false), reset: () => resolve(true), }) }) const canNavigateAsync = await promise setResolver({ status: 'idle', current: undefined, next: undefined, action: undefined, proceed: undefined, reset: undefined, }) return canNavigateAsync } return disabled ? undefined : history.block({ blockerFn: blockerFnComposed, enableBeforeUnload }) }, [ shouldBlockFn, enableBeforeUnload, disabled, withResolver, history, router, ]) return resolver } const _resolvePromptBlockerArgs = ( props: PromptProps | LegacyPromptProps, ): UseBlockerOpts => { if ('shouldBlockFn' in props) { return { ...props } } const shouldBlock = Boolean(props.condition ?? true) const fn = props.blockerFn const _customBlockerFn = async () => { if (shouldBlock && fn !== undefined) { return await fn() } return shouldBlock } return { shouldBlockFn: _customBlockerFn, enableBeforeUnload: shouldBlock, withResolver: fn === undefined, } } export function Block< TRouter extends AnyRouter = RegisteredRouter, TWithResolver extends boolean = boolean, >(opts: PromptProps): React.ReactNode /** * @deprecated Use the UseBlockerOpts property instead */ export function Block(opts: LegacyPromptProps): React.ReactNode export function Block(opts: PromptProps | LegacyPromptProps): React.ReactNode { const { children, ...rest } = opts const args = _resolvePromptBlockerArgs(rest) const resolver = useBlocker(args) return children ? typeof children === 'function' ? children(resolver as any) : children : null } type LegacyPromptProps = { blockerFn?: LegacyBlockerFn condition?: boolean | any children?: React.ReactNode | ((params: BlockerResolver) => React.ReactNode) } type PromptProps< TRouter extends AnyRouter = RegisteredRouter, TWithResolver extends boolean = boolean, TParams = TWithResolver extends true ? BlockerResolver : void, > = UseBlockerOpts & { children?: React.ReactNode | ((params: TParams) => React.ReactNode) }