
Nitro Image is a superfast Image core type and view component for React Native, built with Nitro!
- Powered by Nitro Modules for highly efficient native bindings! 🔥
- Instance-based
Image
type with byte-buffer pixel data access 🔗 - Supports in-memory image operations like resizing and cropping without saving to file 📐
- Supports deferred
ImageLoader
types to optimize for displaying large lists of Images ⏳ - Fast Web Image loading and caching using SDWebImage (iOS) and Coil (Android) 🌎
- ThumbHash support for elegant placeholders 🖼️
function App() {
return (
<NitroImage
image={{ filePath: '/tmp/image.jpg' }}
style={{ width: 400, height: 400 }}
/>
)
}
Install react-native-nitro-image from npm:
npm i react-native-nitro-image
npm i react-native-nitro-modules
cd ios && pod install
Note
Since NitroImage is built with Nitro Views, it requires the new architecture to be enabled.
To keep NitroImage super lightweight, it does not ship a web image loader and caching system. If you want to load images from the web, install react-native-nitro-web-image as well:
npm i react-native-nitro-web-image
cd ios && pod install
Then, since SDWebImage does not enable modular headers for static linkage, you need to enable those yourself in your app's Podfile
:
target '…' do
config = use_native_modules!
# Add this line:
pod 'SDWebImage', :modular_headers => true
There are numerous ways to create an Image
through the Images
factory:
const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
const fileImage = await Images.loadFromFileAsync('file://my-image.jpg')
const resourceImage = Images.loadFromResources('my-resource.jpg')
const symbolImage = Images.loadFromSymbol('star')
When loading from a remote URL, you can tweak options such as priority
:
const image1 = await WebImages.loadFromURLAsync(URL1, { priority: 'low' })
const image2 = await WebImages.loadFromURLAsync(URL2, { priority: 'high' })
The Image
type can be converted to- and from- an ArrayBuffer
, which gives you access to the raw pixel data in ARGB format:
const image = // ...
const arrayBuffer = await image.toArrayBufferAsync()
const sameImageCopied = await Images.loadFromArrayBufferAsync(arrayBuffer)
An Image
can be resized entirely in-memory, without ever writing to- or reading from- a file:
const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
const smaller = await webImage.resizeAsync(200, 200)
An Image
can be cropped entirely in-memory, without ever writing to- or reading from- a file:
const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
const smaller = await webImage.cropAsync(100, 100, 50, 50)
An in-memory Image
object can also be written/saved to a file:
const smaller = ...
const path = await smaller.saveToTemporaryFileAsync('jpg', 90)
The useImage()
hook asynchronously loads an Image
from the given source and returns it as a React state:
function App() {
const image = useImage({ filePath: '/tmp/image.jpg' })
return …
}
The useImageLoader()
hook creates an asynchronous ImageLoader
which can be passed to a <NitroImage />
view to defer image loading:
function App() {
const loader = useImageLoader({ filePath: '/tmp/image.jpg' })
return (
<NitroImage
image={loader}
style={{ width: 400, height: 400 }}
/>
)
}
The <NitroImage />
view is a React Native view that allows you to render Image
- either asynchronously (by wrapping ImageLoader
s), or synchronously (by passing Image
instances directly):
function App() {
return (
<NitroImage
image={{ filePath: '/tmp/image.jpg' }}
style={{ width: 400, height: 400 }}
/>
)
}
The <NativeNitroImage />
view is the actual native Nitro View component for rendering an Image
instance. It is recommended to use abstractions like <NitroImage />
instead of the actual native component. However if you need to use the native component instead, it is still exposed:
function App() {
const image = …
return (
<NativeNitroImage
image={image}
style={{ width: 400, height: 400 }}
/>
)
}
To achieve a dynamic width or height calculation, you can use the image
's dimensions:
function App() {
const image = useImage({ filePath: '/tmp/image.jpg' })
const aspect = (image?.width ?? 1) / (image?.height ?? 1)
return (
<NitroImage
image={image}
style={{ width: '100%', aspectRatio: aspect }}
/>
)
}
This will now resize the height
dimension to match the same aspect ratio as the image
- in this case it will be 1:1 since the image is 400x400.
If the image
is 400x200, the height
of the view will be half of the width
of the view, i.e. a 0.5 aspect ratio.
A ThumbHash is a short binary (or base64 string) representation of a blurry image.
Since it is a very small buffer (or base64 string), it can be added to a payload (like a user
object in your database) to immediately display an image placeholder while the actual image loads.
Usage Example
For example, your users
database could have a users.profile_picture_url
field which you use to asynchronously load the web Image, and a users.profile_picture_thumbhash
field which contains the ThumbHash buffer (or base64 string) which you can display on-device immediately.
users
users.profile_picture_url
: Load asynchronouslyusers.profile_picture_thumbhash
: Decode & Display immediately
Everytime you upload a new profile picture for the user, you should encode the image to a new ThumbHash again and update the users.profile_picture_thumbhash
field. This should ideally happen on your backend, but can also be performed on-device if needed.
NitroImage supports conversion from- and to- ThumbHash representations out of the box.
For performance reasons, a ThumbHash is represented as an ArrayBuffer
.
const thumbHash = // from server
const image = Images.loadFromThumbHash(thumbHash)
const thumbHashAgain = image.toThumbHash()
If your ThumbHash is a string
, convert it to an ArrayBuffer
first, since this is more efficient:
const thumbHashBase64 = // from server
const thumbHashArrayBuffer = thumbHashFromBase64String(thumbHashBase64)
const thumbHashBase64Again = thumbHashToBase64String(thumbHashArrayBuffer)
Since ThumbHash decoding or encoding can be a slow process, you should consider using the async methods instead:
const thumbHash = // from server
const image = await Images.loadFromThumbHashAsync(thumbHash)
const thumbHashAgain = await image.toThumbHash()
To use the native Image
type in your library (e.g. in a Camera library), you need to follow these steps:
- Add the dependency on
react-native-nitro-image
- JS: Add
react-native-nitro-image
topeerDependencies
anddevDependencies
- Android: Add
:react-native-nitro-image
to yourbuild.gradle
'sdependencies
, andreact-native-nitro-image::NitroImage
to your CMake's dependencies (it's a prefab) - iOS: Add
NitroImage
to your*.podspec
's dependencies
- JS: Add
- In your Nitro specs (
*.nitro.ts
), just importImage
from'react-native-nitro-image'
and use it as a type - In your native implementation, you can either;
- Implement
HybridImageSpec
,HybridImageLoaderSpec
orHybridImageViewSpec
with your custom implementation, e.g. to create aImage
implementation that doesn't useUIImage
but instead usesCGImage
, or anAVPhoto
- Use the
HybridImageSpec
,HybridImageLoaderSpec
orHybridImageViewSpec
types. You can either use them abstract (with all the methods that are also exposed to JS), or by downcasting them to a specific type - all of them follow a protocol likeNativeImage
:class HybridCustom: HybridCustomSpec { func doSomething(image: any HybridImageSpec) throws { guard let image = image as? NativeImage else { return } let uiImage = image.uiImage // ... } }
- Implement
- Done! 🎉 Now you can benefit from a common, shared
Image
type - e.g. your Camera library can directly return anImage
instance intakePhoto()
, which can be instantly rendered using<NitroImage />
- no more file I/O!