After implementing a manual OTA (Over-The-Air) update system for my React Native app, I ran into a major limitation: images imported via require()
don’t update with the JS bundle. React Native statically bundles assets during build time, so even after a successful OTA update, the images remained stale.
To solve this, I created a flexible system where image assets are:
- Organized by screen density (
drawable-mdpi
,hdpi
, etc.), - Zipped into a single file,
- Hosted on Firebase Hosting,
- Downloaded and unzipped at runtime,
- Loaded dynamically using file URIs.
Here’s exactly how I did it:
📁 Step 1: Organizing the Asset Bundle (ota-bundles
)
My folder structure looks like this:
📦ota-bundles/
┣ 📂drawable-mdpi/
┃ ┣ 📜assets_workoutimage_triceps_tricepdips.webp
┃ ┣ 📜node_modules_reactnativecalendars_src_img_down.png
┃ ┗ 📜... more images
┣ 📂drawable-hdpi/
┣ 📂drawable-xhdpi/
┣ 📂drawable-xxhdpi/
┣ 📂drawable-xxxhdpi/
┣ 📜index.android.bundle ← OTA JS bundle
┣ 📜version.json ← Version tracking file
┣ 📜index.html, 404.html ← Firebase placeholders
┗ 📜ota-bundles.zip ← Zipped OTA bundle
Inside each drawable-*
folder, I placed all relevant image assets including custom app images and auto-extracted images used by third-party libraries (like react-native-calendars
, react-navigation
, etc.).
📦 Step 2: Zipping the Asset Bundle
Once my folder structure was ready, I zipped the entire ota-bundles
folder:
zip -r ota-bundles.zip ota-bundles/
This produced ota-bundles.zip
, which contains:
-
index.android.bundle
(JS code), -
drawable-*
folders (images), -
version.json
(to detect updates).
This .zip
file is what my app will download during an update.
☁️ Step 3: Hosting the Zip File on Firebase Hosting
Firebase Hosting provides a reliable and fast CDN for serving static files.
Setup
Now host the whole ota-bundles.
Run:
firebase deploy --only hosting
- After deployment, Firebase gives you a public URL:
https://your-app.web.app/ota-bundles.zip
This URL is then used in the app to download the update.
📲 Step 4: Downloading & Unzipping the Assets at Runtime
In your React Native app, you handle this zip file using react-native-fs
and react-native-zip-archive
:
import RNFS from 'react-native-fs';
import { unzip } from 'react-native-zip-archive';
const ZIP_URL = 'https://your-app.web.app/ota-bundles.zip';
const LOCAL_ZIP = `${RNFS.DocumentDirectoryPath}/ota-bundles.zip`;
const EXTRACT_PATH = `${RNFS.DocumentDirectoryPath}/ota-bundles`;
export const downloadAndExtractBundle = async () => {
// Step 1: Download ZIP
const downloadRes = await RNFS.downloadFile({
fromUrl: ZIP_URL,
toFile: LOCAL_ZIP,
}).promise;
if (downloadRes.statusCode === 200) {
// Step 2: Extract
const extractedPath = await unzip(LOCAL_ZIP, EXTRACT_PATH);
await RNFS.unlink(LOCAL_ZIP); // Clean up
console.log('Assets extracted to:', extractedPath);
} else {
console.warn('Failed to download OTA bundle');
}
};
🖼️ Step 5: Loading Images Dynamically
React Native’s require()
doesn’t support dynamic paths. Instead, you use the file://
URI scheme to load images like this:
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import RNFS from 'react-native-fs';
const imagePath = `${RNFS.DocumentDirectoryPath}/ota-bundles/drawable-mdpi/assets_workoutimage_triceps_tricepdips.webp`;
export default function TricepsImage() {
return <Image source={{ uri: `file://${imagePath}` }} style={styles.image} />;
}
const styles = StyleSheet.create({
image: {
width: 120,
height: 120,
resizeMode: 'contain',
},
});
This works across all densities by checking PixelRatio
and dynamically selecting the correct drawable-*
folder if needed.
Top comments (0)