Skip to content

Commit

Permalink
feat: Implement React portability patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
codinsonn committed Apr 6, 2024
1 parent b76bce0 commit d13289d
Show file tree
Hide file tree
Showing 16 changed files with 177 additions and 73 deletions.
16 changes: 15 additions & 1 deletion apps/expo/app/ExpoRootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { Stack } from 'expo-router'
import UniversalAppProviders from '@app/core/screens/UniversalAppProviders'
import UniversalRootLayout from '@app/core/screens/UniversalRootLayout'
import { Link as ExpoContextLink } from '@app/core/navigation/Link.core.native'
import { useRouter as useExpoContextRouter } from '@app/core/navigation/useRouter.core.native'
import { useRouteParams as useExpoRouteParams } from '@app/core/navigation/useRouteParams.core.native'

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

/* --- <ExpoRootLayout/> ----------------------------------------------------------------------- */

export default function ExpoRootLayout() {
// Navigation
const expoContextRouter = useExpoContextRouter()

// -- Render --

return (
<UniversalAppProviders>
<UniversalAppProviders
contextLink={ExpoContextLink}
contextRouter={expoContextRouter}
useContextRouteParams={useExpoRouteParams}
>
<UniversalRootLayout>
<Stack
screenOptions={{
Expand Down
24 changes: 19 additions & 5 deletions apps/next/app/NextClientRootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client'
import React from 'react'
import UniversalAppProviders from '@app/core/screens/UniversalAppProviders'
import { Link as NextContextLink } from '@app/core/navigation/Link.core.web'
import { useRouter as useNextContextRouter } from '@app/core/navigation/useRouter.core.web'
import { useRouteParams as useNextRouteParams } from '@app/core/navigation/useRouteParams.core.web'

// -i- This is a regular react client component
// -i- It's still rendered on the server during SSR, but it also hydrates on the client
Expand All @@ -15,11 +18,22 @@ type NextClientRootLayoutProps = {

/* --- <NextClientRootLayout/> ---------------------------------------------------------------- */

const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => (
<UniversalAppProviders>
{children}
</UniversalAppProviders>
)
const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => {
// Navigation
const nextContextRouter = useNextContextRouter()

// -- Render --

return (
<UniversalAppProviders
contextLink={NextContextLink}
contextRouter={nextContextRouter}
useContextRouteParams={useNextRouteParams}
>
{children}
</UniversalAppProviders>
)
}

/* --- Exports --------------------------------------------------------------------------------- */

Expand Down
26 changes: 26 additions & 0 deletions features/app-core/context/CoreContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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'

// -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 Link component and Router

/* --- Types ----------------------------------------------------------------------------------- */

export type CoreContextType = {
contextLink: (props: UniversalLinkProps) => JSX.Element
contextRouter: UniversalRouterMethods
useContextRouteParams: (routeScreenProps: UniversalRouteScreenProps) => ReturnType<typeof useLocalSearchParams>
}

/* --- Context --------------------------------------------------------------------------------- */

export const CoreContext = React.createContext<CoreContextType>({
contextLink: null,
contextRouter: null,
useContextRouteParams: () => ({}),
})
45 changes: 45 additions & 0 deletions features/app-core/navigation/Link.core.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Link as ExpoLink } from 'expo-router'
import type { UniversalLinkProps } from './Link.types'

/* --- <Link/> --------------------------------------------------------------------------------- */

export const Link = (props: UniversalLinkProps) => {
// Props
const {
children,
href,
style,
replace,
onPress,
target,
asChild,
push,
testID,
nativeID,
allowFontScaling,
numberOfLines,
maxFontSizeMultiplier
} = props

// -- Render --

return (
<ExpoLink
href={href}
style={style}
onPress={onPress}
target={target}
asChild={asChild}
replace={replace}
push={push}
testID={testID}
nativeID={nativeID}
allowFontScaling={allowFontScaling}
numberOfLines={numberOfLines}
maxFontSizeMultiplier={maxFontSizeMultiplier}
>
{children}
</ExpoLink>
)
}

1 change: 1 addition & 0 deletions features/app-core/navigation/Link.core.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Link } from './Link.core.native'
File renamed without changes.
43 changes: 6 additions & 37 deletions features/app-core/navigation/Link.tsx
Original file line number Diff line number Diff line change
@@ -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'

/* --- <Link/> --------------------------------------------------------------------------------- */

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 (
<ExpoLink
href={href}
style={style}
onPress={onPress}
target={target}
asChild={asChild}
replace={replace}
push={push}
testID={testID}
nativeID={nativeID}
allowFontScaling={allowFontScaling}
numberOfLines={numberOfLines}
maxFontSizeMultiplier={maxFontSizeMultiplier}
>
{children}
</ExpoLink>
)
// Render
return <ContextLink {...props} />
}

14 changes: 14 additions & 0 deletions features/app-core/navigation/useRouteParams.core.native.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions features/app-core/navigation/useRouteParams.core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useRouteParams } from './useRouteParams.core.native'
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { UniversalRouteScreenProps } from './useRouteParams.types'
import type { useLocalSearchParams } from 'expo-router'

/** --- useRouteParams() -------------------------------------------------------------- */
/** --- useRouteParams() ----------------------------------------------------------------------- */
/** -i- Gets the route search and query params on both web and mobile */
export const useRouteParams = (routeScreenProps: UniversalRouteScreenProps) => {
const { params, searchParams } = routeScreenProps
return { ...params, ...searchParams }
return { ...params, ...searchParams } as ReturnType<typeof useLocalSearchParams>
}
18 changes: 7 additions & 11 deletions features/app-core/navigation/useRouteParams.ts
Original file line number Diff line number Diff line change
@@ -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() ------------------------------------------------------------------------ */

/** --- 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)
}
15 changes: 15 additions & 0 deletions features/app-core/navigation/useRouter.core.native.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions features/app-core/navigation/useRouter.core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useRouter } from './useRouter.core.native'
17 changes: 7 additions & 10 deletions features/app-core/navigation/useRouter.ts
Original file line number Diff line number Diff line change
@@ -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
}
24 changes: 17 additions & 7 deletions features/app-core/screens/UniversalAppProviders.tsx
Original file line number Diff line number Diff line change
@@ -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
}

/* --- <UniversalAppProviders/> ---------------------------------------------------------------- */

const UniversalAppProviders = ({ children }: UniversalAppProvidersProps) => (
<>
{children}
</>
)
const UniversalAppProviders = (props: UniversalAppProvidersProps) => {
// Props
const { children, contextLink, contextRouter, useContextRouteParams } = props

// -- Render --

return (
<>
<CoreContext.Provider value={{ contextLink, contextRouter, useContextRouteParams }}>
{children}
</CoreContext.Provider>
</>
)
}

/* --- Exports --------------------------------------------------------------------------------- */

Expand Down

0 comments on commit d13289d

Please sign in to comment.