Skip to content

Commit a584810

Browse files
committed
feat: Add portable Image component
1 parent b8239bd commit a584810

File tree

7 files changed

+129
-86
lines changed

7 files changed

+129
-86
lines changed

apps/expo/app/ExpoRootLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Stack } from 'expo-router'
22
import UniversalAppProviders from '@app/core/screens/UniversalAppProviders'
33
import UniversalRootLayout from '@app/core/screens/UniversalRootLayout'
4+
import { Image as ExpoContextImage } from '@app/core/components/Image.expo'
45
import { Link as ExpoContextLink } from '@app/core/navigation/Link.expo'
56
import { useRouter as useExpoContextRouter } from '@app/core/navigation/useRouter.expo'
67
import { useRouteParams as useExpoRouteParams } from '@app/core/navigation/useRouteParams.expo'
@@ -20,6 +21,7 @@ export default function ExpoRootLayout() {
2021

2122
return (
2223
<UniversalAppProviders
24+
contextImage={ExpoContextImage}
2325
contextLink={ExpoContextLink}
2426
contextRouter={expoContextRouter}
2527
useContextRouteParams={useExpoRouteParams}

apps/next/app/NextClientRootLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22
import React from 'react'
33
import UniversalAppProviders from '@app/core/screens/UniversalAppProviders'
4+
import { Image as NextContextImage } from '@app/core/components/Image.next'
45
import { Link as NextContextLink } from '@app/core/navigation/Link.next'
56
import { useRouter as useNextContextRouter } from '@app/core/navigation/useRouter.next'
67
import { useRouteParams as useNextRouteParams } from '@app/core/navigation/useRouteParams.next'
@@ -26,6 +27,7 @@ const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => {
2627

2728
return (
2829
<UniversalAppProviders
30+
contextImage={NextContextImage}
2931
contextLink={NextContextLink}
3032
contextRouter={nextContextRouter}
3133
useContextRouteParams={useNextRouteParams}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Image as ExpoImage } from 'expo-image'
2+
import { UniversalImageProps, UniversalImageMethods } from './Image.types'
3+
import { Platform } from 'react-native'
4+
5+
/* --- <Image/> -------------------------------------------------------------------------------- */
6+
7+
const Image = (props: UniversalImageProps): JSX.Element => {
8+
// Props
9+
const {
10+
/* - Universal - */
11+
src,
12+
alt,
13+
width,
14+
height,
15+
style,
16+
priority,
17+
onError,
18+
onLoadEnd,
19+
/* - Split - */
20+
expoPlaceholder,
21+
/* - Next.js - */
22+
onLoad,
23+
fill,
24+
/* - Expo - */
25+
accessibilityLabel,
26+
accessible,
27+
allowDownscaling,
28+
autoplay,
29+
blurRadius,
30+
cachePolicy,
31+
contentFit,
32+
contentPosition,
33+
enableLiveTextInteraction,
34+
focusable,
35+
onLoadStart,
36+
onProgress,
37+
placeholderContentFit,
38+
recyclingKey,
39+
responsivePolicy,
40+
} = props
41+
42+
// -- Overrides --
43+
44+
// @ts-ignore
45+
const finalStyle = { width, height, ...style }
46+
if (fill) finalStyle.height = '100%'
47+
if (fill) finalStyle.width = '100%'
48+
49+
// -- Render --
50+
51+
return (
52+
<ExpoImage
53+
/* - Universal - */
54+
source={src as any}
55+
alt={alt || accessibilityLabel} // @ts-ignore
56+
style={finalStyle}
57+
priority={priority}
58+
onError={onError}
59+
onLoadEnd={onLoadEnd || onLoad as any}
60+
/* - Split - */
61+
placeholder={expoPlaceholder}
62+
/* - Expo - */
63+
accessibilityLabel={alt || accessibilityLabel}
64+
accessible={accessible}
65+
blurRadius={blurRadius}
66+
cachePolicy={cachePolicy}
67+
contentFit={contentFit}
68+
contentPosition={contentPosition}
69+
focusable={focusable}
70+
onLoadStart={onLoadStart}
71+
onProgress={onProgress}
72+
placeholderContentFit={placeholderContentFit}
73+
recyclingKey={recyclingKey}
74+
responsivePolicy={responsivePolicy}
75+
/* - Platform diffs - */
76+
{...(Platform.select({
77+
web: {},
78+
native: {
79+
autoplay,
80+
enableLiveTextInteraction,
81+
allowDownscaling,
82+
},
83+
}))}
84+
/>
85+
)
86+
}
87+
88+
/* --- Static Methods -------------------------------------------------------------------------- */
89+
90+
Image.clearDiskCache = ExpoImage.clearDiskCache as UniversalImageMethods['clearDiskCache']
91+
Image.clearMemoryCache = ExpoImage.clearMemoryCache as UniversalImageMethods['clearMemoryCache']
92+
Image.getCachePathAsync = ExpoImage.getCachePathAsync as UniversalImageMethods['getCachePathAsync']
93+
Image.prefetch = ExpoImage.prefetch as UniversalImageMethods['prefetch']
94+
95+
/* --- Exports --------------------------------------------------------------------------------- */
96+
97+
export { Image }

features/app-core/components/Image.tsx

Lines changed: 15 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,22 @@
1-
import { Image as ExpoImage } from 'expo-image'
2-
import { UniversalImageProps, UniversalImageMethods } from './Image.types'
1+
import React from 'react'
2+
import type { UniversalImageProps, UniversalImageMethods } from './Image.types'
3+
import { CoreContext } from '../context/CoreContext'
34

4-
/* --- <Image/> -------------------------------------------------------------------------------- */
5+
/* --- <Image/> --------------------------------------------------------------------------------- */
56

6-
const Image = (props: UniversalImageProps): JSX.Element => {
7-
// Props
8-
const {
9-
/* - Universal - */
10-
src,
11-
alt,
12-
width,
13-
height,
14-
style,
15-
priority,
16-
onError,
17-
onLoadEnd,
18-
/* - Split - */
19-
expoPlaceholder,
20-
/* - Next.js - */
21-
onLoad,
22-
fill,
23-
/* - Expo - */
24-
accessibilityLabel,
25-
accessible,
26-
allowDownscaling,
27-
autoplay,
28-
blurRadius,
29-
cachePolicy,
30-
contentFit,
31-
contentPosition,
32-
enableLiveTextInteraction,
33-
focusable,
34-
onLoadStart,
35-
onProgress,
36-
placeholderContentFit,
37-
recyclingKey,
38-
responsivePolicy,
39-
} = props
7+
const Image = ((props: UniversalImageProps) => {
8+
// Context
9+
const { contextImage: ContextImage } = React.useContext(CoreContext)
4010

41-
// -- Overrides --
11+
// Static methods
12+
if (!Image.clearDiskCache) Image.clearDiskCache = ContextImage.clearDiskCache
13+
if (!Image.clearMemoryCache) Image.clearMemoryCache = ContextImage.clearMemoryCache
14+
if (!Image.getCachePathAsync) Image.getCachePathAsync = ContextImage.getCachePathAsync
15+
if (!Image.prefetch) Image.prefetch = ContextImage.prefetch
4216

43-
// @ts-ignore
44-
const finalStyle = { width, height, ...style }
45-
if (fill) finalStyle.height = '100%'
46-
if (fill) finalStyle.width = '100%'
47-
48-
// -- Render --
49-
50-
return (
51-
<ExpoImage
52-
/* - Universal - */
53-
source={src as any}
54-
alt={alt || accessibilityLabel} // @ts-ignore
55-
style={finalStyle}
56-
priority={priority}
57-
onError={onError}
58-
onLoadEnd={onLoadEnd || onLoad as any}
59-
/* - Split - */
60-
placeholder={expoPlaceholder}
61-
/* - Expo - */
62-
accessibilityLabel={alt || accessibilityLabel}
63-
accessible={accessible}
64-
allowDownscaling={allowDownscaling}
65-
autoplay={autoplay}
66-
blurRadius={blurRadius}
67-
cachePolicy={cachePolicy}
68-
contentFit={contentFit}
69-
contentPosition={contentPosition}
70-
enableLiveTextInteraction={enableLiveTextInteraction}
71-
focusable={focusable}
72-
onLoadStart={onLoadStart}
73-
onProgress={onProgress}
74-
placeholderContentFit={placeholderContentFit}
75-
recyclingKey={recyclingKey}
76-
responsivePolicy={responsivePolicy}
77-
/>
78-
)
79-
}
80-
81-
/* --- Static Methods -------------------------------------------------------------------------- */
82-
83-
Image.clearDiskCache = ExpoImage.clearDiskCache as UniversalImageMethods['clearDiskCache']
84-
Image.clearMemoryCache = ExpoImage.clearMemoryCache as UniversalImageMethods['clearMemoryCache']
85-
Image.getCachePathAsync = ExpoImage.getCachePathAsync as UniversalImageMethods['getCachePathAsync']
86-
Image.prefetch = ExpoImage.prefetch as UniversalImageMethods['prefetch']
17+
// Render
18+
return <ContextImage {...props} />
19+
}) as ((props: UniversalImageProps) => JSX.Element) & UniversalImageMethods
8720

8821
/* --- Exports --------------------------------------------------------------------------------- */
8922

features/app-core/context/CoreContext.ts renamed to features/app-core/context/CoreContext.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,33 @@ import { UniversalLinkProps } from '../navigation/Link.types'
33
import { UniversalRouterMethods } from '../navigation/useRouter.types'
44
import { UniversalRouteScreenProps } from '../navigation/useRouteParams.types'
55
import type { useLocalSearchParams } from 'expo-router'
6+
import { UniversalImageMethods, UniversalImageProps } from '../components/Image.types'
67

78
// -i- This context's only aim is to provide React Portability & Framework Ejection patterns if required
89
// -i- By allowing you to provide your own custom Link and Router overrides, you could e.g.:
910
// -i- 1) Support Expo for Web by not defaulting to Next.js's Link and Router on web
10-
// -i- 2) Eject from Next.js entirely and e.g. use another framework's Link component and Router
11+
// -i- 2) Eject from Next.js entirely and e.g. use another framework's Image / Link / router
1112

1213
/* --- Types ----------------------------------------------------------------------------------- */
1314

1415
export type CoreContextType = {
16+
contextImage: ((props: UniversalImageProps) => JSX.Element) & UniversalImageMethods
1517
contextLink: (props: UniversalLinkProps) => JSX.Element
1618
contextRouter: UniversalRouterMethods
1719
useContextRouteParams: (routeScreenProps: UniversalRouteScreenProps) => ReturnType<typeof useLocalSearchParams>
1820
}
1921

22+
/* --- Dummy ----------------------------------------------------------------------------------- */
23+
24+
const createDummyComponent = (contextComponentName: string) => (props: any) => {
25+
throw new Error(`CoreContext was not provided with a ${contextComponentName}. Please provide one in UniversalAppProviders.`)
26+
}
27+
2028
/* --- Context --------------------------------------------------------------------------------- */
2129

2230
export const CoreContext = React.createContext<CoreContextType>({
23-
contextLink: null,
31+
contextImage: createDummyComponent('contextImage') as any,
32+
contextLink: createDummyComponent('contextLink'),
2433
contextRouter: null,
2534
useContextRouteParams: () => ({}),
2635
})

features/app-core/screens/UniversalAppProviders.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ type UniversalAppProvidersProps = CoreContextType & {
1717

1818
const UniversalAppProviders = (props: UniversalAppProvidersProps) => {
1919
// Props
20-
const { children, contextLink, contextRouter, useContextRouteParams } = props
20+
const { children, contextImage, contextLink, contextRouter, useContextRouteParams } = props
2121

2222
// -- Render --
2323

2424
return (
2525
<>
26-
<CoreContext.Provider value={{ contextLink, contextRouter, useContextRouteParams }}>
26+
<CoreContext.Provider value={{ contextImage, contextLink, contextRouter, useContextRouteParams }}>
2727
{children}
2828
</CoreContext.Provider>
2929
</>

0 commit comments

Comments
 (0)