The Offline Maps feature allows you to download map regions for offline use, enabling your app to function without network connectivity. The SDK provides a comprehensive API for creating, managing, and monitoring offline map packs.
Overview
Offline maps are managed through the offlineManager singleton, which provides methods for:
- Creating and downloading offline packs
- Monitoring download progress
- Managing existing packs
- Invalidating and updating cached tiles
- Controlling cache size
Basic Setup
Import offlineManager
import { offlineManager } from '@rnmapbox/maps';
Create an offline pack
await offlineManager.createPack(
{
name: 'my-offline-region',
styleURL: 'mapbox://styles/mapbox/streets-v11',
bounds: [[-74.2, 40.7], [-73.8, 41.0]],
minZoom: 10,
maxZoom: 16,
},
progressListener,
errorListener
);
Monitor progress
const progressListener = (pack, status) => {
console.log(`Downloaded: ${status.percentage}%`);
};
const errorListener = (pack, error) => {
console.error('Download error:', error);
};
Creating Offline Packs
Pack Options
Define what region and zoom levels to download:
import { offlineManager, StyleURL } from '@rnmapbox/maps';
const createOfflinePack = async () => {
const packOptions = {
name: 'manhattan-streets',
styleURL: StyleURL.Street,
bounds: [
[-74.0479, 40.6829], // Southwest corner [lng, lat]
[-73.9067, 40.8820], // Northeast corner [lng, lat]
],
minZoom: 10,
maxZoom: 16,
};
await offlineManager.createPack(
packOptions,
onProgress,
onError
);
};
Pack Options Reference
| Option | Type | Required | Description |
|---|
name | string | Yes | Unique identifier for the pack |
styleURL | string | Yes | Map style URL |
bounds | [[lng, lat], [lng, lat]] | Yes | Southwest and northeast corners |
minZoom | number | Yes | Minimum zoom level to download |
maxZoom | number | Yes | Maximum zoom level to download |
The tile count increases exponentially with zoom level. Be cautious when setting high maxZoom values for large areas.
Monitoring Downloads
Progress Listener
Track download progress with detailed status information:
import { useState } from 'react';
import { View, Text, ProgressBar } from 'react-native';
import { offlineManager, OfflinePackDownloadState } from '@rnmapbox/maps';
const OfflineDownloader = () => {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState('idle');
const onDownloadProgress = (pack, downloadStatus) => {
setProgress(downloadStatus.percentage);
const state = downloadStatus.state;
if (state === OfflinePackDownloadState.Active) {
setStatus('downloading');
} else if (state === OfflinePackDownloadState.Complete) {
setStatus('complete');
} else {
setStatus('inactive');
}
console.log('Progress:', {
percentage: downloadStatus.percentage,
completedTileCount: downloadStatus.completedTileCount,
completedResourceCount: downloadStatus.completedResourceCount,
completedResourceSize: downloadStatus.completedResourceSize,
requiredResourceCount: downloadStatus.requiredResourceCount,
});
};
const onError = (pack, error) => {
console.error('Download failed:', error.message);
setStatus('error');
};
const startDownload = async () => {
await offlineManager.createPack(
{
name: `offline-pack-${Date.now()}`,
styleURL: StyleURL.Street,
bounds: [[-74.2, 40.7], [-73.8, 41.0]],
minZoom: 10,
maxZoom: 14,
},
onDownloadProgress,
onError
);
};
return (
<View>
<Text>Status: {status}</Text>
<ProgressBar progress={progress / 100} />
<Text>Downloaded: {progress.toFixed(1)}%</Text>
</View>
);
};
Progress Status Properties
type OfflineProgressStatus = {
name: string; // Pack name
state: number; // Download state
percentage: number; // Completion percentage (0-100)
completedResourceSize: number; // Bytes downloaded
completedTileCount: number; // Tiles downloaded
completedResourceCount: number; // Resources downloaded
requiredResourceCount: number; // Total resources needed
completedTileSize: number; // Total tile size in bytes
};
Managing Offline Packs
Retrieving Packs
// Get all packs
const allPacks = await offlineManager.getPacks();
console.log(`Found ${allPacks.length} offline packs`);
// Get specific pack by name
const pack = await offlineManager.getPack('manhattan-streets');
if (pack) {
const status = await pack.status();
console.log('Pack status:', status);
}
Pack Methods
const pack = await offlineManager.getPack('my-pack');
// Get pack status
const status = await pack.status();
// Pause download
await pack.pause();
// Resume download
await pack.resume();
Deleting Packs
// Delete a specific pack
await offlineManager.deletePack('manhattan-streets');
// Delete all packs
const packs = await offlineManager.getPacks();
for (const pack of packs) {
await offlineManager.deletePack(pack.name);
}
Invalidating Packs
Update offline tiles without re-downloading everything:
// Invalidate checks if local tiles match server tiles
// Only downloads tiles that have changed
await offlineManager.invalidatePack('manhattan-streets');
invalidatePack is more efficient than deleting and re-downloading. It only fetches tiles that have been updated on the server.
Complete Example
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import {
MapView,
Camera,
offlineManager,
StyleURL,
OfflinePackDownloadState,
} from '@rnmapbox/maps';
const OfflineMapExample = () => {
const [packs, setPacks] = useState([]);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
useEffect(() => {
loadExistingPacks();
return () => {
// Cleanup listeners
offlineManager.unsubscribe('my-offline-pack');
};
}, []);
const loadExistingPacks = async () => {
const existingPacks = await offlineManager.getPacks();
setPacks(existingPacks);
};
const downloadRegion = async () => {
setDownloading(true);
setProgress(0);
const packName = `offline-pack-${Date.now()}`;
try {
await offlineManager.createPack(
{
name: packName,
styleURL: StyleURL.Street,
bounds: [[-74.0479, 40.6829], [-73.9067, 40.8820]],
minZoom: 10,
maxZoom: 14,
},
(pack, status) => {
setProgress(status.percentage);
if (status.state === OfflinePackDownloadState.Complete) {
setDownloading(false);
loadExistingPacks();
}
},
(pack, error) => {
console.error('Download error:', error);
setDownloading(false);
}
);
} catch (error) {
console.error('Failed to create pack:', error);
setDownloading(false);
}
};
const deletePack = async (packName) => {
await offlineManager.deletePack(packName);
loadExistingPacks();
};
return (
<View style={styles.container}>
<MapView style={styles.map}>
<Camera
centerCoordinate={[-73.9772, 40.7527]}
zoomLevel={12}
/>
</MapView>
<View style={styles.controls}>
<Button
title={downloading ? `Downloading ${progress.toFixed(0)}%` : 'Download Region'}
onPress={downloadRegion}
disabled={downloading}
/>
<Text style={styles.title}>Offline Packs: {packs.length}</Text>
{packs.map((pack) => (
<View key={pack.name} style={styles.packItem}>
<Text>{pack.name}</Text>
<Button title="Delete" onPress={() => deletePack(pack.name)} />
</View>
))}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
controls: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
},
title: { fontSize: 16, fontWeight: 'bold', marginVertical: 10 },
packItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 5,
},
});
export default OfflineMapExample;
Advanced Features
Setting Tile Count Limit
Limit the maximum number of tiles that can be downloaded:
// Set maximum tiles (Mapbox ToS requirement)
offlineManager.setTileCountLimit(6000);
The Mapbox Terms of Service prohibit changing or bypassing the tile count limit without permission from Mapbox.
Progress Event Throttling
Control how often progress events are emitted:
// Set progress event interval to 500ms (default is 300ms)
offlineManager.setProgressEventThrottle(500);
Subscribing to Existing Packs
Attach listeners to already-created packs:
const pack = await offlineManager.getPack('existing-pack');
// Subscribe to updates
await offlineManager.subscribe(
'existing-pack',
(pack, status) => console.log('Progress:', status.percentage),
(pack, error) => console.error('Error:', error)
);
// Resume download
await pack.resume();
Merging Offline Regions
Sideload offline database from another source:
// Path to offline database file
const dbPath = '/path/to/offline.db';
await offlineManager.mergeOfflineRegions(dbPath);
Reset Database
Delete all offline data and reinitialize:
await offlineManager.resetDatabase();
resetDatabase() permanently deletes all offline packs and cached data. Use with caution.
Cache Management
Set Maximum Cache Size
// Set maximum ambient cache to 50MB
await offlineManager.setMaximumAmbientCacheSize(50 * 1024 * 1024);
// Disable ambient cache
await offlineManager.setMaximumAmbientCacheSize(0);
Calculating Bounds
Use helper libraries to calculate bounds from center coordinates:
import geoViewport from '@mapbox/geo-viewport';
import { Dimensions } from 'react-native';
const calculateBounds = (centerCoordinate, zoom) => {
const { width, height } = Dimensions.get('window');
const bounds = geoViewport.bounds(
centerCoordinate,
zoom,
[width, height],
512 // Tile size
);
return [
[bounds[0], bounds[1]], // Southwest
[bounds[2], bounds[3]], // Northeast
];
};
const bounds = calculateBounds([-73.9772, 40.7527], 12);
Best Practices
- Start with lower zoom levels (10-14) for testing
- Calculate tile counts before downloading large regions
- Provide UI feedback during downloads
- Handle errors gracefully with retry logic
- Clean up listeners in component unmount
- Use
invalidatePack instead of delete + re-download
- Test offline functionality thoroughly
Reference
- Offline Manager:
src/modules/offline/offlineManager.ts:1
- Pack Options:
src/modules/offline/OfflineCreatePackOptions.ts:1
- Offline Pack:
src/modules/offline/OfflinePack.ts:1