diff --git a/apps/expo/app/ExpoRootLayout.tsx b/apps/expo/app/ExpoRootLayout.tsx index b8bd9f5..e7440d0 100644 --- a/apps/expo/app/ExpoRootLayout.tsx +++ b/apps/expo/app/ExpoRootLayout.tsx @@ -1,15 +1,31 @@ import { Stack } from 'expo-router' import UniversalAppProviders from '@app/core/screens/UniversalAppProviders' import UniversalRootLayout from '@app/core/screens/UniversalRootLayout' +import { Image as ExpoContextImage } from '@app/core/components/Image.expo' +import { Link as ExpoContextLink } from '@app/core/navigation/Link.expo' +import { useRouter as useExpoContextRouter } from '@app/core/navigation/useRouter.expo' +import { useRouteParams as useExpoRouteParams } from '@app/core/navigation/useRouteParams.expo' // -i- Expo Router's layout setup is much simpler than Next.js's layout setup // -i- Since Expo doesn't require a custom document setup or server component root layout // -i- Use this file to apply your Expo specific layout setup: // -i- like rendering our Universal Layout and App Providers +/* --- ----------------------------------------------------------------------- */ + export default function ExpoRootLayout() { + // Navigation + const expoContextRouter = useExpoContextRouter() + + // -- Render -- + return ( - + ---------------------------------------------------------------- */ -const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => ( - - {children} - -) +const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => { + // Navigation + const nextContextRouter = useNextContextRouter() + + // -- Render -- + + return ( + + {children} + + ) +} /* --- Exports --------------------------------------------------------------------------------- */ diff --git a/features/app-core/components/Image.expo.tsx b/features/app-core/components/Image.expo.tsx new file mode 100644 index 0000000..7179b39 --- /dev/null +++ b/features/app-core/components/Image.expo.tsx @@ -0,0 +1,97 @@ +import { Image as ExpoImage } from 'expo-image' +import { UniversalImageProps, UniversalImageMethods } from './Image.types' +import { Platform } from 'react-native' + +/* --- -------------------------------------------------------------------------------- */ + +const Image = (props: UniversalImageProps): JSX.Element => { + // Props + const { + /* - Universal - */ + src, + alt, + width, + height, + style, + priority, + onError, + onLoadEnd, + /* - Split - */ + expoPlaceholder, + /* - Next.js - */ + onLoad, + fill, + /* - Expo - */ + accessibilityLabel, + accessible, + allowDownscaling, + autoplay, + blurRadius, + cachePolicy, + contentFit, + contentPosition, + enableLiveTextInteraction, + focusable, + onLoadStart, + onProgress, + placeholderContentFit, + recyclingKey, + responsivePolicy, + } = props + + // -- Overrides -- + + // @ts-ignore + const finalStyle = { width, height, ...style } + if (fill) finalStyle.height = '100%' + if (fill) finalStyle.width = '100%' + + // -- Render -- + + return ( + + ) +} + +/* --- Static Methods -------------------------------------------------------------------------- */ + +Image.clearDiskCache = ExpoImage.clearDiskCache as UniversalImageMethods['clearDiskCache'] +Image.clearMemoryCache = ExpoImage.clearMemoryCache as UniversalImageMethods['clearMemoryCache'] +Image.getCachePathAsync = ExpoImage.getCachePathAsync as UniversalImageMethods['getCachePathAsync'] +Image.prefetch = ExpoImage.prefetch as UniversalImageMethods['prefetch'] + +/* --- Exports --------------------------------------------------------------------------------- */ + +export { Image } diff --git a/features/app-core/components/Image.web.tsx b/features/app-core/components/Image.next.tsx similarity index 100% rename from features/app-core/components/Image.web.tsx rename to features/app-core/components/Image.next.tsx diff --git a/features/app-core/components/Image.tsx b/features/app-core/components/Image.tsx index 71bf58f..a4ac6d0 100644 --- a/features/app-core/components/Image.tsx +++ b/features/app-core/components/Image.tsx @@ -1,96 +1,22 @@ -import { Image as ExpoImage } from 'expo-image' -import { Platform } from 'react-native' -import { UniversalImageProps, UniversalImageMethods } from './Image.types' +import React from 'react' +import type { UniversalImageProps, UniversalImageMethods } from './Image.types' +import { CoreContext } from '../context/CoreContext' -/* --- -------------------------------------------------------------------------------- */ +/* --- --------------------------------------------------------------------------------- */ -const Image = (props: UniversalImageProps): JSX.Element => { - // Props - const { - /* - Universal - */ - src, - alt, - width, - height, - style, - priority, - onError, - onLoadEnd, - /* - Split - */ - expoPlaceholder, - /* - Next.js - */ - onLoad, - fill, - /* - Expo - */ - accessibilityLabel, - accessible, - allowDownscaling, - autoplay, - blurRadius, - cachePolicy, - contentFit, - contentPosition, - enableLiveTextInteraction, - focusable, - onLoadStart, - onProgress, - placeholderContentFit, - recyclingKey, - responsivePolicy, - } = props +const Image = ((props: UniversalImageProps) => { + // Context + const { contextImage: ContextImage } = React.useContext(CoreContext) - // -- Overrides -- + // Static methods + if (!Image.clearDiskCache) Image.clearDiskCache = ContextImage.clearDiskCache + if (!Image.clearMemoryCache) Image.clearMemoryCache = ContextImage.clearMemoryCache + if (!Image.getCachePathAsync) Image.getCachePathAsync = ContextImage.getCachePathAsync + if (!Image.prefetch) Image.prefetch = ContextImage.prefetch - // @ts-ignore - const finalStyle = { width, height, ...style } - if (fill) finalStyle.height = '100%' - if (fill) finalStyle.width = '100%' - - // -- Render -- - - return ( - - ) -} - -/* --- Static Methods -------------------------------------------------------------------------- */ - -Image.clearDiskCache = ExpoImage.clearDiskCache as UniversalImageMethods['clearDiskCache'] -Image.clearMemoryCache = ExpoImage.clearMemoryCache as UniversalImageMethods['clearMemoryCache'] -Image.getCachePathAsync = ExpoImage.getCachePathAsync as UniversalImageMethods['getCachePathAsync'] -Image.prefetch = ExpoImage.prefetch as UniversalImageMethods['prefetch'] + // Render + return +}) as ((props: UniversalImageProps) => JSX.Element) & UniversalImageMethods /* --- Exports --------------------------------------------------------------------------------- */ diff --git a/features/app-core/context/CoreContext.tsx b/features/app-core/context/CoreContext.tsx new file mode 100644 index 0000000..57d3bf9 --- /dev/null +++ b/features/app-core/context/CoreContext.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { UniversalLinkProps } from '../navigation/Link.types' +import { UniversalRouterMethods } from '../navigation/useRouter.types' +import { UniversalRouteScreenProps } from '../navigation/useRouteParams.types' +import type { useLocalSearchParams } from 'expo-router' +import { UniversalImageMethods, UniversalImageProps } from '../components/Image.types' + +// -i- This context's only aim is to provide React Portability & Framework Ejection patterns if required +// -i- By allowing you to provide your own custom Link and Router overrides, you could e.g.: +// -i- 1) Support Expo for Web by not defaulting to Next.js's Link and Router on web +// -i- 2) Eject from Next.js entirely and e.g. use another framework's Image / Link / router + +/* --- Types ----------------------------------------------------------------------------------- */ + +export type CoreContextType = { + contextImage: ((props: UniversalImageProps) => JSX.Element) & UniversalImageMethods + contextLink: (props: UniversalLinkProps) => JSX.Element + contextRouter: UniversalRouterMethods + useContextRouteParams: (routeScreenProps: UniversalRouteScreenProps) => ReturnType +} + +/* --- Dummy ----------------------------------------------------------------------------------- */ + +const createDummyComponent = (contextComponentName: string) => (props: any) => { + throw new Error(`CoreContext was not provided with a ${contextComponentName}. Please provide one in UniversalAppProviders.`) +} + +/* --- Context --------------------------------------------------------------------------------- */ + +export const CoreContext = React.createContext({ + contextImage: createDummyComponent('contextImage') as any, + contextLink: createDummyComponent('contextLink'), + contextRouter: null, + useContextRouteParams: () => ({}), +}) diff --git a/features/app-core/navigation/Link.expo.tsx b/features/app-core/navigation/Link.expo.tsx new file mode 100644 index 0000000..6f36026 --- /dev/null +++ b/features/app-core/navigation/Link.expo.tsx @@ -0,0 +1,45 @@ +import { Link as ExpoLink } from 'expo-router' +import type { UniversalLinkProps } from './Link.types' + +/* --- --------------------------------------------------------------------------------- */ + +export const Link = (props: UniversalLinkProps) => { + // Props + const { + children, + href, + style, + replace, + onPress, + target, + asChild, + push, + testID, + nativeID, + allowFontScaling, + numberOfLines, + maxFontSizeMultiplier + } = props + + // -- Render -- + + return ( + + {children} + + ) +} + diff --git a/features/app-core/navigation/Link.web.tsx b/features/app-core/navigation/Link.next.tsx similarity index 100% rename from features/app-core/navigation/Link.web.tsx rename to features/app-core/navigation/Link.next.tsx diff --git a/features/app-core/navigation/Link.tsx b/features/app-core/navigation/Link.tsx index 6f36026..271d490 100644 --- a/features/app-core/navigation/Link.tsx +++ b/features/app-core/navigation/Link.tsx @@ -1,45 +1,14 @@ -import { Link as ExpoLink } from 'expo-router' +import React from 'react' import type { UniversalLinkProps } from './Link.types' +import { CoreContext } from '../context/CoreContext' /* --- --------------------------------------------------------------------------------- */ export const Link = (props: UniversalLinkProps) => { - // Props - const { - children, - href, - style, - replace, - onPress, - target, - asChild, - push, - testID, - nativeID, - allowFontScaling, - numberOfLines, - maxFontSizeMultiplier - } = props + // Context + const { contextLink: ContextLink } = React.useContext(CoreContext) - // -- Render -- - - return ( - - {children} - - ) + // Render + return } diff --git a/features/app-core/navigation/useRouteParams.expo.ts b/features/app-core/navigation/useRouteParams.expo.ts new file mode 100644 index 0000000..e7d4374 --- /dev/null +++ b/features/app-core/navigation/useRouteParams.expo.ts @@ -0,0 +1,14 @@ +import { useLocalSearchParams } from 'expo-router' +import type { UniversalRouteScreenProps } from './useRouteParams.types' + +/** --- useRouteParams() ----------------------------------------------------------------------- */ +/** -i- Gets the route search and query params on both web and mobile */ +export const useRouteParams = (routeScreenProps: UniversalRouteScreenProps) => { + const { params, searchParams } = routeScreenProps + const expoRouterParams = useLocalSearchParams() + return { + ...params, + ...searchParams, + ...expoRouterParams, + } as typeof expoRouterParams +} diff --git a/features/app-core/navigation/useRouteParams.web.ts b/features/app-core/navigation/useRouteParams.next.ts similarity index 90% rename from features/app-core/navigation/useRouteParams.web.ts rename to features/app-core/navigation/useRouteParams.next.ts index ae9cf24..7427436 100644 --- a/features/app-core/navigation/useRouteParams.web.ts +++ b/features/app-core/navigation/useRouteParams.next.ts @@ -1,5 +1,6 @@ import type { useLocalSearchParams } from 'expo-router' import type { UniversalRouteScreenProps } from './useRouteParams.types' +import type { useLocalSearchParams } from 'expo-router' /** --- useRouteParams() ----------------------------------------------------------------------- */ /** -i- Gets the route search and query params on both web and mobile */ diff --git a/features/app-core/navigation/useRouteParams.ts b/features/app-core/navigation/useRouteParams.ts index e7d4374..7307ad1 100644 --- a/features/app-core/navigation/useRouteParams.ts +++ b/features/app-core/navigation/useRouteParams.ts @@ -1,14 +1,10 @@ -import { useLocalSearchParams } from 'expo-router' -import type { UniversalRouteScreenProps } from './useRouteParams.types' +import { useContext } from 'react' +import { CoreContext } from '../context/CoreContext' +import { UniversalRouteScreenProps } from './useRouteParams.types' /** --- useRouteParams() ----------------------------------------------------------------------- */ /** -i- Gets the route search and query params on both web and mobile */ export const useRouteParams = (routeScreenProps: UniversalRouteScreenProps) => { - const { params, searchParams } = routeScreenProps - const expoRouterParams = useLocalSearchParams() - return { - ...params, - ...searchParams, - ...expoRouterParams, - } as typeof expoRouterParams + const { useContextRouteParams } = useContext(CoreContext) + return useContextRouteParams(routeScreenProps) } diff --git a/features/app-core/navigation/useRouter.expo.ts b/features/app-core/navigation/useRouter.expo.ts new file mode 100644 index 0000000..320a990 --- /dev/null +++ b/features/app-core/navigation/useRouter.expo.ts @@ -0,0 +1,15 @@ +import { router } from 'expo-router' +import { UniversalRouterMethods } from './useRouter.types' + +/* --- useRouter() ----------------------------------------------------------------------------- */ + +export const useRouter = () => { + return { + push: router.push, + navigate: router.navigate, + replace: router.replace, + back: router.back, + canGoBack: router.canGoBack, + setParams: router.setParams, + } as UniversalRouterMethods +} diff --git a/features/app-core/navigation/useRouter.web.ts b/features/app-core/navigation/useRouter.next.ts similarity index 100% rename from features/app-core/navigation/useRouter.web.ts rename to features/app-core/navigation/useRouter.next.ts diff --git a/features/app-core/navigation/useRouter.ts b/features/app-core/navigation/useRouter.ts index 320a990..e981594 100644 --- a/features/app-core/navigation/useRouter.ts +++ b/features/app-core/navigation/useRouter.ts @@ -1,15 +1,12 @@ -import { router } from 'expo-router' -import { UniversalRouterMethods } from './useRouter.types' +import { useContext } from 'react' +import { CoreContext } from '../context/CoreContext' /* --- useRouter() ----------------------------------------------------------------------------- */ export const useRouter = () => { - return { - push: router.push, - navigate: router.navigate, - replace: router.replace, - back: router.back, - canGoBack: router.canGoBack, - setParams: router.setParams, - } as UniversalRouterMethods + // Context + const { contextRouter } = useContext(CoreContext) + + // Return + return contextRouter } diff --git a/features/app-core/screens/UniversalAppProviders.tsx b/features/app-core/screens/UniversalAppProviders.tsx index 770f906..04c9132 100644 --- a/features/app-core/screens/UniversalAppProviders.tsx +++ b/features/app-core/screens/UniversalAppProviders.tsx @@ -1,24 +1,34 @@ 'use client' import React from 'react' +import { CoreContext, CoreContextType } from '../context/CoreContext' // -i- This is a regular react client component -// -i- Use this file for adding universal app providers +// -i- Use this file for adding universal app providers that work in both Expo and Next.js // -i- It will be rendered by 'apps/expo' on mobile from the 'ExpoRootLayout' component // -i- It will also be rendered by 'apps/next' on web from the 'NextClientRootLayout' component /* --- Types ----------------------------------------------------------------------------------- */ -type UniversalAppProvidersProps = { +type UniversalAppProvidersProps = CoreContextType & { children: React.ReactNode } /* --- ---------------------------------------------------------------- */ -const UniversalAppProviders = ({ children }: UniversalAppProvidersProps) => ( - <> - {children} - -) +const UniversalAppProviders = (props: UniversalAppProvidersProps) => { + // Props + const { children, contextImage, contextLink, contextRouter, useContextRouteParams } = props + + // -- Render -- + + return ( + <> + + {children} + + + ) +} /* --- Exports --------------------------------------------------------------------------------- */