one
Version:
One is a new React Framework that makes Vite serve both native and web.
154 lines (132 loc) • 4.05 kB
text/typescript
import { useNavigation } from '@react-navigation/native'
import * as React from 'react'
export type BlockerState = 'unblocked' | 'blocked' | 'proceeding'
export type BlockerFunction = (args: {
currentLocation: string
nextLocation: string
historyAction: 'push' | 'pop' | 'replace'
}) => boolean
export type Blocker =
| {
state: 'unblocked'
reset?: undefined
proceed?: undefined
location?: undefined
}
| {
state: 'blocked'
reset: () => void
proceed: () => void
location: string
}
| {
state: 'proceeding'
reset?: undefined
proceed?: undefined
location: string
}
/**
* Block navigation when a condition is met.
*
* On native, this uses React Navigation's `beforeRemove` event to prevent navigation.
* Note that this only works for navigation within the app - it cannot prevent
* the app from being closed or backgrounded.
*
* @param shouldBlock - Either a boolean or a function that returns whether to block.
*
* @example
* ```tsx
* function EditForm() {
* const [isDirty, setIsDirty] = useState(false)
* const blocker = useBlocker(isDirty)
*
* return (
* <>
* <TextInput onChange={() => setIsDirty(true)} />
*
* {blocker.state === 'blocked' && (
* <Modal>
* <Text>You have unsaved changes. Leave anyway?</Text>
* <Button title="Stay" onPress={blocker.reset} />
* <Button title="Leave" onPress={blocker.proceed} />
* </Modal>
* )}
* </>
* )
* }
* ```
*/
export function useBlocker(shouldBlock: BlockerFunction | boolean): Blocker {
const navigation = useNavigation()
const [state, setState] = React.useState<BlockerState>('unblocked')
const [pendingEvent, setPendingEvent] = React.useState<any>(null)
const [blockedLocation, setBlockedLocation] = React.useState<string | null>(null)
const shouldBlockRef = React.useRef(shouldBlock)
shouldBlockRef.current = shouldBlock
React.useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
const currentShouldBlock = shouldBlockRef.current
// Get the next location from the action payload
const payload = e.data?.action?.payload as { name?: string } | undefined
const nextLocation = payload?.name || 'previous screen'
// Determine if we should block
const block =
typeof currentShouldBlock === 'function'
? currentShouldBlock({
currentLocation: '', // Not easily available on native
nextLocation,
historyAction: 'pop',
})
: currentShouldBlock
if (!block) {
return
}
// Prevent default behavior (leaving the screen)
e.preventDefault()
// Store the event to dispatch later if user confirms
setPendingEvent(e)
setBlockedLocation(nextLocation)
setState('blocked')
})
return unsubscribe
}, [navigation])
const reset = React.useCallback(() => {
setPendingEvent(null)
setBlockedLocation(null)
setState('unblocked')
}, [])
const proceed = React.useCallback(() => {
if (!pendingEvent) return
setState('proceeding')
// Dispatch the original action to complete navigation
navigation.dispatch(pendingEvent.data.action)
// Reset after navigation
setTimeout(() => {
setPendingEvent(null)
setBlockedLocation(null)
setState('unblocked')
}, 100)
}, [navigation, pendingEvent])
if (state === 'unblocked') {
return { state: 'unblocked' }
}
if (state === 'proceeding') {
return { state: 'proceeding', location: blockedLocation! }
}
return {
state: 'blocked',
reset,
proceed,
location: blockedLocation!,
}
}
/**
* No-op on native - native uses React Navigation's beforeRemove event instead.
* This is only used by the router on web.
*/
export function checkBlocker(
_nextLocation: string,
_historyAction: 'push' | 'pop' | 'replace' = 'push'
): boolean {
return false
}