Files
RN_Template/RN_TEMPLATE/app/navigators/navigationUtilities.ts
2026-02-05 13:16:05 +08:00

209 lines
7.2 KiB
TypeScript

import { useState, useEffect, useRef } from "react"
import { BackHandler, Linking, Platform } from "react-native"
import {
NavigationState,
PartialState,
createNavigationContainerRef,
} from "@react-navigation/native"
import Config from "@/config"
import type { PersistNavigationConfig } from "@/config/config.base"
import * as storage from "@/utils/storage"
import { useIsMounted } from "@/utils/useIsMounted"
import type { AppStackParamList, NavigationProps } from "./navigationTypes"
type Storage = typeof storage
/**
* Reference to the root App Navigator.
*
* If needed, you can use this to access the navigation object outside of a
* `NavigationContainer` context. However, it's recommended to use the `useNavigation` hook whenever possible.
* @see [Navigating Without Navigation Prop]{@link https://reactnavigation.org/docs/navigating-without-navigation-prop/}
*
* The types on this reference will only let you reference top level navigators. If you have
* nested navigators, you'll need to use the `useNavigation` with the stack navigator's ParamList type.
*/
export const navigationRef = createNavigationContainerRef<AppStackParamList>()
/**
* Gets the current screen from any navigation state.
* @param {NavigationState | PartialState<NavigationState>} state - The navigation state to traverse.
* @returns {string} - The name of the current screen.
*/
export function getActiveRouteName(state: NavigationState | PartialState<NavigationState>): string {
const route = state.routes[state.index ?? 0]
// Found the active route -- return the name
if (!route.state) return route.name as keyof AppStackParamList
// Recursive call to deal with nested routers
return getActiveRouteName(route.state as NavigationState<AppStackParamList>)
}
const iosExit = () => false
/**
* Hook that handles Android back button presses and forwards those on to
* the navigation or allows exiting the app.
* @see [BackHandler]{@link https://reactnative.dev/docs/backhandler}
* @param {(routeName: string) => boolean} canExit - Function that returns whether we can exit the app.
* @returns {void}
*/
export function useBackButtonHandler(canExit: (routeName: string) => boolean) {
// The reason we're using a ref here is because we need to be able
// to update the canExit function without re-setting up all the listeners
const canExitRef = useRef(Platform.OS !== "android" ? iosExit : canExit)
useEffect(() => {
canExitRef.current = canExit
}, [canExit])
useEffect(() => {
// We'll fire this when the back button is pressed on Android.
const onBackPress = () => {
if (!navigationRef.isReady()) {
return false
}
// grab the current route
const routeName = getActiveRouteName(navigationRef.getRootState())
// are we allowed to exit?
if (canExitRef.current(routeName)) {
// exit and let the system know we've handled the event
BackHandler.exitApp()
return true
}
// we can't exit, so let's turn this into a back action
if (navigationRef.canGoBack()) {
navigationRef.goBack()
return true
}
return false
}
// Subscribe when we come to life
const subscription = BackHandler.addEventListener("hardwareBackPress", onBackPress)
// Unsubscribe when we're done
return () => subscription.remove()
}, [])
}
/**
* This helper function will determine whether we should enable navigation persistence
* based on a config setting and the __DEV__ environment (dev or prod).
* @param {PersistNavigationConfig} persistNavigation - The config setting for navigation persistence.
* @returns {boolean} - Whether to restore navigation state by default.
*/
function navigationRestoredDefaultState(persistNavigation: PersistNavigationConfig) {
if (persistNavigation === "always") return false
if (persistNavigation === "dev" && __DEV__) return false
if (persistNavigation === "prod" && !__DEV__) return false
// all other cases, disable restoration by returning true
return true
}
/**
* Custom hook for persisting navigation state.
* @param {Storage} storage - The storage utility to use.
* @param {string} persistenceKey - The key to use for storing the navigation state.
* @returns {object} - The navigation state and persistence functions.
*/
export function useNavigationPersistence(storage: Storage, persistenceKey: string) {
const [initialNavigationState, setInitialNavigationState] =
useState<NavigationProps["initialState"]>()
const isMounted = useIsMounted()
const initNavState = navigationRestoredDefaultState(Config.persistNavigation)
const [isRestored, setIsRestored] = useState(initNavState)
const routeNameRef = useRef<keyof AppStackParamList | undefined>(undefined)
const onNavigationStateChange = (state: NavigationState | undefined) => {
const previousRouteName = routeNameRef.current
if (state !== undefined) {
const currentRouteName = getActiveRouteName(state)
if (previousRouteName !== currentRouteName) {
// track screens.
if (__DEV__) {
console.log(currentRouteName)
}
}
// Save the current route name for later comparison
routeNameRef.current = currentRouteName as keyof AppStackParamList
// Persist state to storage
storage.save(persistenceKey, state)
}
}
const restoreState = async () => {
try {
const initialUrl = await Linking.getInitialURL()
// Only restore the state if app has not started from a deep link
if (!initialUrl) {
const state = (await storage.load(persistenceKey)) as NavigationProps["initialState"] | null
if (state) setInitialNavigationState(state)
}
} finally {
if (isMounted()) setIsRestored(true)
}
}
useEffect(() => {
if (!isRestored) restoreState()
// runs once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return { onNavigationStateChange, restoreState, isRestored, initialNavigationState }
}
/**
* use this to navigate without the navigation
* prop. If you have access to the navigation prop, do not use this.
* @see {@link https://reactnavigation.org/docs/navigating-without-navigation-prop/}
* @param {unknown} name - The name of the route to navigate to.
* @param {unknown} params - The params to pass to the route.
*/
export function navigate(name: unknown, params?: unknown) {
if (navigationRef.isReady()) {
// @ts-expect-error
navigationRef.navigate(name as never, params as never)
}
}
/**
* This function is used to go back in a navigation stack, if it's possible to go back.
* If the navigation stack can't go back, nothing happens.
* The navigationRef variable is a React ref that references a navigation object.
* The navigationRef variable is set in the App component.
*/
export function goBack() {
if (navigationRef.isReady() && navigationRef.canGoBack()) {
navigationRef.goBack()
}
}
/**
* resetRoot will reset the root navigation state to the given params.
* @param {Parameters<typeof navigationRef.resetRoot>[0]} state - The state to reset the root to.
* @returns {void}
*/
export function resetRoot(
state: Parameters<typeof navigationRef.resetRoot>[0] = { index: 0, routes: [] },
) {
if (navigationRef.isReady()) {
navigationRef.resetRoot(state)
}
}