How Galeria works (BYOIC™)
masterreact-native, expo-image, next/image, solito/image, or standard HTML <img> tags on the web.repository·master·Indexed 21 days ago
https://github.com/nandorojo/galeriaA high-performance image viewer for React and React Native featuring shared element transitions, pinch-to-zoom, and multi-image support. It utilizes a 'Bring Your Own Image Component' (BYOIC™) pattern, allowing compatibility with react-native, expo-image, next/image, solito/image, and standard HTML <img> tags. Requires New Architecture (Fabric), Expo SDK 54+ or React Native 0.79+, and iOS 16.4+.
react-native, expo-image, next/image, solito/image, or standard HTML <img> tags on the web.Install the package using your preferred package manager.
Requirements:
Platform Specific Setup:
@nandorojo/galeria to transpilePackages in your next.config.js.npx expo prebuild and then npx expo run:ios or npx expo run:android to rebuild native code.yarn add @nandorojo/galeria
# or
npm i @nandorojo/galeriaThe GaleriaContext object defines the internal state and configuration for the Galeria component. While typically managed internally, understanding its shape is useful for developers extending the library or interacting with the context.
Key properties include:
initialIndex: The starting image index.open: Boolean indicating if the gallery is visible.urls: An array of ImageSource (strings or React Native asset sources).theme: Either 'dark' or 'light'.src: The source string for the current image.hideBlurOverlay: Boolean to toggle the blur overlay.hidePageIndicators: Boolean to toggle page indicators.setOpen: A function used to transition the gallery state. It accepts an object with { open: true, src: string, initialIndex: number, id?: string } to open the gallery, or { open: false } to close it.export const GaleriaContext = createContext({
initialIndex: 0,
open: false,
urls: [] as unknown as undefined | ImageSource[],
closeIconName: undefined as undefined | SFSymbol,
setOpen: (
info:
| { open: true; src: string; initialIndex: number; id?: string }
| { open: false }
) => {},
theme: 'dark' as 'dark' | 'light',
src: '',
hideBlurOverlay: false,
hidePageIndicators: false,
})On the web, Galeria is a pure React component library that uses Framer Motion and does not rely on React Native code. You can use standard HTML <img> tags as your image component.
Note: Web support currently only supports viewing a single image at a time.
import { Galeria } from '@nandorojo/galeria'
const urls = ['https://my-image.com/image.jpg']
export const WebSupport = () => (
<Galeria urls={urls}>
<Galeria.Image>
<img src={urls[0]} width={100} height={100} />
</Galeria.Image>
</Galeria>
)For multiple images, pass an array of URLs to the urls prop. You must map through the URLs and provide a Galeria.Image component for each, passing the corresponding index to ensure correct synchronization.
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'react-native'
import localImage from './assets/local-image.png'
const urls = ['https://my-image.com/image.jpg', localImage]
export const MutliImage = ({ style }) => (
<Galeria urls={urls}>
{urls.map((url, index) => (
<Galeria.Image index={index} key={...}>
<Image source={typeof url === 'string' ? { uri: url } : url} style={style} />
</Galeria.Image>
))}
</Galeria>
)Galeria is compatible with @shopify/flash-list. When using it within a list, wrap the item component inside Galeria.Image and pass the item's index to the Galeria.Image component.
Note: It is recommended to memoize the component returned by renderItem for performance.
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'react-native'
import { FlashList } from '@shopify/flash-list'
const urls = ['https://my-image.com/image.jpg', localImage]
const size = 100
export const FlashListSupport = () => {
return (
<Galeria urls={urls}>
<FlashList
data={urls}
renderItem={({ item, index }) => {
return (
<Galeria.Image index={index}>
<Image
source={src(item)}
style={{ width: size, height: size }}
/>
</Galeria.Image>
)
}}
numColumns={3}
estimatedItemSize={size}
keyExtractor={(item, i) => item + i}
/>
</Galeria>
)
}Set the theme prop on the Galeria component to 'dark' to enable dark mode styling.
import { Galeria } from '@nandorojo/galeria'
export const DarkMode = () => (
<Galeria urls={urls} theme="dark">
...
</Galeria>
)To display a single image, wrap your image component with Galeria.Image inside a Galeria provider. Pass the image URL(s) as an array to the urls prop of Galeria.
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'react-native'
const url = 'https://my-image.com/image.jpg'
export const SingleImage = ({ style }) => (
<Galeria urls={[url]}>
<Galeria.Image>
<Image source={{ uri: url }} style={style} />
</Galeria.Image>
</Galeria>
)When using Galeria in a monorepo or a project structure where react-native might be installed in a parent node_modules directory, you must configure metro.config.js to prevent version conflicts and ensure the bundler watches the correct folders.
Specifically, you should:
react-native: Add the parent node_modules/react-native to the resolver.blockList to prevent the bundler from using an incompatible version from the parent directory.nodeModulesPaths: Explicitly include both the local ./node_modules and the parent ../node_modules in the resolver paths.watchFolders: Add the parent directory to watchFolders so Metro tracks changes in the workspace.inlineRequires: Set inlineRequires: true in the transformer options for optimized performance.const { getDefaultConfig } = require('expo/metro-config')
const path = require('path')
const config = getDefaultConfig(__dirname)
// Prevent incompatible react-native versions from parent folder
config.resolver.blockList = [
...Array.from(config.resolver.blockList ?? []),
new RegExp(path.resolve('..', 'node_modules', 'react-native')),
]
// Ensure both local and parent node_modules are searched
config.resolver.nodeModulesPaths = [
path.resolve(__dirname, './node_modules'),
path.resolve(__dirname, '../node_modules'),
]
// Watch the parent directory for changes
config.watchFolders = [path.resolve(__dirname, '..')]
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
})
module.exports = configThe expo-module.config.json file defines the platform support and native module entry points for the Galeria library. This configuration allows the library to be integrated into Expo projects by specifying the native module classes for iOS and Android, and declaring support for web.
{
"platforms": ["ios", "android", "web"],
"ios": {
"modules": ["GaleriaModule"]
},
"android": {
"modules": ["nandorojo.modules.galeria.GaleriaModule"]
}
}When using Galeria on iOS, you can use the following props on Galeria.Image to customize the viewer UI:
hideBlurOverlay: Hides the blur overlay that appears behind the image viewer.hidePageIndicators: Hides the page indicator dots when viewing multiple images.// Hide Blur Overlay
<Galeria.Image hideBlurOverlay>
<Image source={{ uri: url }} style={style} />
</Galeria.Image>
// Hide Page Indicators
<Galeria.Image hidePageIndicators>
<Image source={{ uri: url }} style={style} />
</Galeria.Image>Galeria provides several event handlers to respond to user interactions:
onIndexChange: Fired when the currently viewed image changes. The event payload contains currentIndex.onLongPress: Fired when the inline trigger image is long-pressed (before the viewer opens).onPressRightNavItemIcon: Fired when the right navigation item icon is pressed. The event payload contains the index of the pressed item.onDismiss: Fired when the gallery viewer is dismissed.interface GaleriaViewProps {
onIndexChange?: (event: GaleriaIndexChangedEvent) => void;
onLongPress?: (event: GaleriaLongPressEvent) => void;
onPressRightNavItemIcon?: (event: GaleriaRightNavItemPressedEvent) => void;
onDismiss?: (event: GaleriaDismissEvent) => void;
}