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 --------------------------------------------------------------------------------- */