From 21aad511b8c305f815ef0a5ff80b6d749bd048f5 Mon Sep 17 00:00:00 2001 From: Mohamed Fouad Sagha Date: Mon, 1 Jan 2024 15:12:53 +0100 Subject: [PATCH] WIP - Trying to use Next App Router --- .gitignore | 6 +- src/router/next-app-router.ts | 3 + src/router/parse-app-next-path.ts | 51 ++++++++++ src/router/use-app-router.ts | 138 ++++++++++++++++++++++++++ src/router/use-navigation.ts | 1 + src/router/use-next-app-router.ts | 4 + src/router/use-next-app-router.web.ts | 1 + src/router/use-router.ts | 3 +- 8 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 src/router/next-app-router.ts create mode 100644 src/router/parse-app-next-path.ts create mode 100644 src/router/use-app-router.ts create mode 100644 src/router/use-next-app-router.ts create mode 100644 src/router/use-next-app-router.web.ts diff --git a/.gitignore b/.gitignore index caa45b9c..d807065b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,8 @@ create-test-app !.yarn/plugins !.yarn/releases !.yarn/sdks -!.yarn/versions \ No newline at end of file +!.yarn/versions + + +.idea +.idea/** diff --git a/src/router/next-app-router.ts b/src/router/next-app-router.ts new file mode 100644 index 00000000..68d6ab3a --- /dev/null +++ b/src/router/next-app-router.ts @@ -0,0 +1,3 @@ +import Router from 'next/navigation' + +export const NextAppRouter: typeof Router | undefined = Router diff --git a/src/router/parse-app-next-path.ts b/src/router/parse-app-next-path.ts new file mode 100644 index 00000000..619434fd --- /dev/null +++ b/src/router/parse-app-next-path.ts @@ -0,0 +1,51 @@ +// import type { NextRouter } from 'next/router' +import {AppRouterInstance} from "next/dist/shared/lib/app-router-context.shared-runtime"; +import {usePathname} from "next/navigation"; + +const parseNextAppPath = (from: Parameters[0]) => { + + console.log(from); + // replace each instance of [key] with the corresponding value from query[key] + // this ensures we're navigating to the correct URL + // it currently ignores [[...param]] + // but I can't see why you would use this with RN + Next.js + if (typeof from == 'object' && from.query && typeof from.query == 'object') { + const query = { ...from.query } + // replace dynamic routes + // and [...param] syntax + for (const key in query) { + if (path.includes(`[${key}]`)) { + path = path.replace(`[${key}]`, `${query[key] ?? ''}`) + delete query[key] + } else if (path.includes(`[...${key}]`)) { + const values = query[key] + if (Array.isArray(values)) { + path = path.replace(`[...${key}]`, values.join('/')) + delete query[key] + } + } + } + + if (Object.keys(query).length) { + // add query param separator + path += '?' + for (const key in query) { + const value = query[key] + if (Array.isArray(value)) { + value.forEach((item) => { + path += `${key}=${item}&` + }) + } else if (value != null) { + path += `${key}=${value}&` + } + } + if (path.endsWith('&') || path.endsWith('?')) { + path = path.slice(0, -1) + } + } + } + + return path +} + +export { parseNextAppPath } diff --git a/src/router/use-app-router.ts b/src/router/use-app-router.ts new file mode 100644 index 00000000..b023901a --- /dev/null +++ b/src/router/use-app-router.ts @@ -0,0 +1,138 @@ +import {AppRouterInstance} from "next/dist/shared/lib/app-router-context.shared-runtime"; +type NextRouterType = AppRouterInstance + +import { Platform } from 'react-native' +import { useContext, useMemo } from 'react' + +import { parseNextAppPath } from './parse-app-next-path' +import { + getActionFromState, + getStateFromPath, + LinkingContext, + StackActions, +} from './replace-helpers' + +import { useLinkTo } from './use-link-to' +import { useNavigation } from './use-navigation' +import {useNextAppRouter} from "./use-next-app-router.web"; +import {usePathname} from "next/navigation"; + +// copied from next/router to appease typescript error +// if we don't manually write this here, then we get some ReturnType error on build +// 🤷‍♂️ +interface TransitionOptions { + shallow?: boolean + locale?: string | false + scroll?: boolean +} + +export function useAppRouter() { + const linkTo = useLinkTo() + const navigation = useNavigation() + const nextRouter: AppRouterInstance = useNextAppRouter() + const linking = useContext(LinkingContext) + const pathName = usePathname() + + return useMemo( + () => ({ + push: ( + url: Parameters[0], + transitionOptions?: TransitionOptions + ) => { + if (Platform.OS === 'web') { + nextRouter?.push(url, transitionOptions) + } else { + const to = parseNextAppPath(url) // get the link to be redirected to in react native. + + if (to) { + linkTo(to) + } + } + }, + replace: ( + url: Parameters[0], + as?: Parameters[1], + transitionOptions?: TransitionOptions & { + experimental?: + | { + nativeBehavior?: undefined + } + | { + nativeBehavior: 'stack-replace' + isNestedNavigator: boolean + } + } + ) => { + if (Platform.OS === 'web') { + nextRouter?.replace(url, transitionOptions) + } else { + const to = parseNextAppPath(url) + + if (to) { + if (transitionOptions?.experimental?.nativeBehavior === 'stack-replace') { + if (linking?.options) { + // custom logic to create a replace() from a URL on native + // https://github.com/react-navigation/react-navigation/discussions/10517 + const { options } = linking + + const state = options?.getStateFromPath + ? options.getStateFromPath(to, options.config) + : getStateFromPath(to, options?.config) + + if (state) { + const action = getActionFromState(state, options?.config) + + if (action !== undefined) { + if ( + 'payload' in action && + action.payload && + 'name' in action.payload && + action.payload.name + ) { + const { name, params } = action.payload + if ( + transitionOptions?.experimental?.isNestedNavigator && + params && + 'screen' in params && + params.screen + ) { + navigation?.dispatch( + StackActions.replace( + params.screen, + params.params as object | undefined + ) + ) + } else { + navigation?.dispatch(StackActions.replace(name, params)) + } + } else { + navigation?.dispatch(action) + } + } else { + navigation?.reset(state) + } + } + } else { + // fallback in case the linking context didn't work + console.warn(`[solito] replace("${to}") faced an issue. You should still see your new screen, but it probably didn't replace the previous one. This may be due to a breaking change in React Navigation. + Please open an issue at https://github.com/nandorojo/solito and report how this happened. Thanks!`) + linkTo(to) + } + } else { + linkTo(to) + } + } + } + }, + back: () => { + if (Platform.OS === 'web') { + nextRouter?.back() + } else { + navigation?.goBack() + } + }, + parseNextAppPath, + }), + [linkTo, navigation] + ) +} diff --git a/src/router/use-navigation.ts b/src/router/use-navigation.ts index 3351c11a..2bb86c63 100644 --- a/src/router/use-navigation.ts +++ b/src/router/use-navigation.ts @@ -4,6 +4,7 @@ import { } from '@react-navigation/core' import { useContext } from 'react' + export const useNavigation = () => { const root = useContext(NavigationContainerRefContext) const navigation = useContext(NavigationContext) diff --git a/src/router/use-next-app-router.ts b/src/router/use-next-app-router.ts new file mode 100644 index 00000000..048a2752 --- /dev/null +++ b/src/router/use-next-app-router.ts @@ -0,0 +1,4 @@ +import { useRouter } from 'next/navigation' + +export const useNextRouter = (): ReturnType | undefined => + undefined diff --git a/src/router/use-next-app-router.web.ts b/src/router/use-next-app-router.web.ts new file mode 100644 index 00000000..2ab616d7 --- /dev/null +++ b/src/router/use-next-app-router.web.ts @@ -0,0 +1 @@ +export { useRouter as useNextAppRouter } from 'next/navigation' diff --git a/src/router/use-router.ts b/src/router/use-router.ts index 84164d18..7a3e2609 100644 --- a/src/router/use-router.ts +++ b/src/router/use-router.ts @@ -25,11 +25,10 @@ interface TransitionOptions { export function useRouter() { const linkTo = useLinkTo() const navigation = useNavigation() - const nextRouter = useNextRouter() - const linking = useContext(LinkingContext) + return useMemo( () => ({ push: (