DEV Community

Arijit das
Arijit das

Posted on

🚨 Fixing Image Asset Issues in OTA Updates for React Native: Overcoming Stale Assets After Updates

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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

  1. Now host the whole ota-bundles.

  2. Run:

firebase deploy --only hosting
Enter fullscreen mode Exit fullscreen mode
  1. After deployment, Firebase gives you a public URL:
   https://your-app.web.app/ota-bundles.zip
Enter fullscreen mode Exit fullscreen mode

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');
  }
};
Enter fullscreen mode Exit fullscreen mode

🖼️ 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',
  },
});
Enter fullscreen mode Exit fullscreen mode

This works across all densities by checking PixelRatio and dynamically selecting the correct drawable-* folder if needed.



Top comments (0)