Skip to content

Commit

Permalink
feat(router): add useblocker
Browse files Browse the repository at this point in the history
  • Loading branch information
xmaxcooking committed Jun 24, 2024
1 parent a3879f3 commit e834283
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 25 deletions.
96 changes: 75 additions & 21 deletions packages/router/src/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,66 @@ export interface NavigateOptions {
replace?: boolean
}

export type BlockerCallback = (tx: { retry: () => void }) => void
type Blocker = {
id: string;

Check warning on line 7 in packages/router/src/history.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Delete `;`
callback: BlockerCallback;

Check warning on line 8 in packages/router/src/history.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Delete `;`
}

const createHistory = () => {
type Listener = (ev?: PopStateEvent) => any

const listeners: Record<string, Listener> = {}
const blockers: Blocker[] = []

return {
const history = {
listen: (listener: Listener) => {
const listenerId = 'RW_HISTORY_LISTENER_ID_' + Date.now()
listeners[listenerId] = listener
globalThis.addEventListener('popstate', listener)
return listenerId
},
navigate: (to: string, options?: NavigateOptions) => {
const { pathname, search, hash } = new URL(
globalThis?.location?.origin + to,
)

if (
globalThis?.location?.pathname !== pathname ||
globalThis?.location?.search !== search ||
globalThis?.location?.hash !== hash
) {
if (options?.replace) {
globalThis.history.replaceState({}, '', to)
} else {
globalThis.history.pushState({}, '', to)
const performNavigation = () => {
const { pathname, search, hash } = new URL(
globalThis?.location?.origin + to,
)

if (
globalThis?.location?.pathname !== pathname ||
globalThis?.location?.search !== search ||
globalThis?.location?.hash !== hash
) {
if (options?.replace) {
globalThis.history.replaceState({}, '', to)
} else {
globalThis.history.pushState({}, '', to)
}
}

for (const listener of Object.values(listeners)) {
listener()
}
}

for (const listener of Object.values(listeners)) {
listener()
if (blockers.length > 0) {
processBlockers(0, performNavigation)
} else {
performNavigation()
}
},
back: () => {
globalThis.history.back()
const performBack = () => {
globalThis.history.back()
for (const listener of Object.values(listeners)) {
listener()
}
}

for (const listener of Object.values(listeners)) {
listener()
if (blockers.length > 0) {
processBlockers(0, performBack)
} else {
performBack()
}
},
remove: (listenerId: string) => {
Expand All @@ -53,11 +75,43 @@ const createHistory = () => {
)
}
},
block: (id: string, callback: BlockerCallback) => {
const existingBlockerIndex = blockers.findIndex(blocker => blocker.id === id)

Check warning on line 79 in packages/router/src/history.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Replace `blocker·=>·blocker.id·===·id` with `⏎········(blocker)·=>·blocker.id·===·id,⏎······`
if (existingBlockerIndex !== -1) {
blockers[existingBlockerIndex] = { id, callback }
} else {
blockers.push({ id, callback })
}
},
unblock: (id: string) => {
const index = blockers.findIndex(blocker => blocker.id === id)

Check warning on line 87 in packages/router/src/history.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Replace `blocker` with `(blocker)`
if (index !== -1) {
blockers.splice(index, 1)
}
}

Check warning on line 91 in packages/router/src/history.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Insert `,`
}

const processBlockers = (index: number, navigate: () => void) => {
if (index < blockers.length) {
blockers[index].callback({
retry: () => processBlockers(index + 1, navigate)

Check warning on line 97 in packages/router/src/history.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Insert `,`
})
} else {
navigate()
}
}

globalThis.addEventListener('beforeunload', (event: BeforeUnloadEvent) => {
if (blockers.length > 0) {
event.preventDefault()
}
})

return history
}

const gHistory = createHistory()

const { navigate, back } = gHistory
const { navigate, back, block, unblock } = gHistory

export { gHistory, navigate, back }
export { gHistory, navigate, back, block, unblock }
1 change: 1 addition & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './route-focus'
export * from './useRouteName'
export * from './useRoutePaths'
export * from './useMatch'
export * from './useBlocker'

export { parseSearch, getRouteRegexAndParams, matchPath } from './util'

Expand Down
9 changes: 5 additions & 4 deletions packages/router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@

import React, { forwardRef } from 'react'

import { navigate } from './history'
import { NavigateOptions, navigate } from './history'

Check failure on line 8 in packages/router/src/link.tsx

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Import "NavigateOptions" is only used as types

export interface LinkProps {
to: string
onClick?: React.MouseEventHandler<HTMLAnchorElement>
options?: NavigateOptions
}

export const Link = forwardRef<
HTMLAnchorElement,
LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
>(({ to, onClick, ...rest }, ref) => (
>(({ to, onClick, options, ...rest }, ref) => (
<a
href={to}
ref={ref}
Expand All @@ -36,10 +37,10 @@ export const Link = forwardRef<
if (onClick) {
const result = onClick(event)
if (typeof result !== 'boolean' || result) {
navigate(to)
navigate(to, options)
}
} else {
navigate(to)
navigate(to, options)
}
}}
/>
Expand Down
50 changes: 50 additions & 0 deletions packages/router/src/useBlocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useCallback, useState, useRef } from 'react'

Check failure on line 1 in packages/router/src/useBlocker.ts

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

There should be at least one empty line between import groups
import { block, unblock, BlockerCallback } from './history'

Check failure on line 2 in packages/router/src/useBlocker.ts

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Import "BlockerCallback" is only used as types

type BlockerState = 'IDLE' | 'BLOCKED'

interface UseBlockerOptions {
when: boolean
}

export function useBlocker({ when }: UseBlockerOptions) {
const [blockerState, setBlockerState] = useState<BlockerState>('IDLE')
const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null)

Check warning on line 12 in packages/router/src/useBlocker.ts

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Replace `(()·=>·void)·|·null` with `⏎····(()·=>·void)·|·null⏎··`
const blockerId = useRef(`BLOCKER_${Date.now()}${Math.random()}`)

const blocker: BlockerCallback = useCallback(
({ retry }) => {
if (when) {
setBlockerState('BLOCKED')
setPendingNavigation(() => retry)
} else {
retry()
}
},
[when]

Check warning on line 24 in packages/router/src/useBlocker.ts

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

Insert `,`
)

useEffect(() => {
if (when) {
block(blockerId.current, blocker)
} else {
unblock(blockerId.current)
}
return () => unblock(blockerId.current)

Check warning on line 33 in packages/router/src/useBlocker.ts

View workflow job for this annotation

GitHub Actions / 🏗 Build, lint, test / ubuntu-latest / node 20 latest

The ref value 'blockerId.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'blockerId.current' to a variable inside the effect, and use that variable in the cleanup function
}, [when, blocker])

const confirm = useCallback(() => {
setBlockerState('IDLE')
if (pendingNavigation) {
pendingNavigation()
setPendingNavigation(null)
}
}, [pendingNavigation])

const abort = useCallback(() => {
setBlockerState('IDLE')
setPendingNavigation(null)
}, [])

return { state: blockerState, confirm, abort }
}

0 comments on commit e834283

Please sign in to comment.