Native zip and unzip for React Native and Expo (iOS & Android). Password protection, progress events, selective extract, and AbortSignal cancellation.
| React Native | Install |
|---|---|
| < 0.70 | npm install react-native-zip-archive@^7.0.0 |
| 0.70–0.81 | Latest v9 (old architecture works; rebuild native) |
| 0.82+ | Latest v9 (New Architecture only) |
iOS: v7+ requires deployment target iOS 15.5+.
New Architecture is recommended. See MIGRATION.md for upgrades from v7.
| Minimum | |
|---|---|
| React Native | ≥ 0.70 |
| React | ≥ 18 |
| iOS | ≥ 15.5 |
| Android | API 23+ |
Published version and peer ranges: see package.json.
npm install react-native-zip-archive
cd ios && pod installWorks in development builds / EAS only — not Expo Go (custom native code).
npx expo install react-native-zip-archiveAdd the config plugin in app.json:
{
"expo": {
"plugins": ["react-native-zip-archive"]
}
}See playground-expo for a working example.
import {
zip,
unzip,
zipWithPassword,
unzipWithPassword,
listContents,
subscribe,
cancel,
ErrorCodes,
} from 'react-native-zip-archive'
// Paths: use react-native-fs (bare) or expo-file-system/legacy (Expo)
import { DocumentDirectoryPath } from 'react-native-fs'
// Expo: const DocumentDirectoryPath = FileSystem.documentDirectory
const archive = `${DocumentDirectoryPath}/bundle.zip`
const outDir = `${DocumentDirectoryPath}/out`
// Zip a folder
await zip(DocumentDirectoryPath, archive)
// Unzip
await unzip(archive, outDir)
// Password
await zipWithPassword(DocumentDirectoryPath, archive, 'secret', 'STANDARD')
await unzipWithPassword(archive, outDir, 'secret')
// List + selective extract + AbortSignal
const controller = new AbortController()
const entries = await listContents(archive)
const assets = entries
.filter((e) => !e.isDirectory && e.path.startsWith('assets/'))
.map((e) => e.path)
await unzip(archive, outDir, { entries: assets, signal: controller.signal })
// controller.abort() → rejects with ZipError code ERR_CANCELLEDProgress and cancel:
const sub = subscribe(({ progress, filePath }) => {
console.log(progress, filePath) // progress: 0…1
})
await unzip(archive, outDir)
sub.remove()
// Or abort the in-flight native op
await cancel() // rejects with ErrorCodes.CANCELLEDZip a folder (string) or files/folders (string[]) to target.
- Single file:
zip([file], target). - Array items may be directories; contents are added recursively (entry paths relative to that directory; empty dirs preserved).
- Third arg: compression level (
0–9, or constants below) or{ compressionLevel, signal }.
import { BEST_SPEED } from 'react-native-zip-archive'
await zip(sourceDir, targetZip)
await zip([fileA, fileB], targetZip, BEST_SPEED)
await zip(sourceDir, targetZip, { compressionLevel: BEST_SPEED, signal })Compression constants: DEFAULT_COMPRESSION (-1), NO_COMPRESSION (0), BEST_SPEED (1), BEST_COMPRESSION (9).
Same sources as zip, with a password.
Encryption types:
| Value | Meaning |
|---|---|
'STANDARD' (default) |
ZipCrypto — readable by Node, Java, stock unzip |
'AES-128' / 'AES-256' |
WinZip-AES (stronger; many server tools cannot open) |
On iOS, both AES options use AES-256 internally. Prefer 'STANDARD' when archives will be unzipped off-device.
await zipWithPassword(sourceDir, targetZip, 'password', 'STANDARD')
await zipWithPassword(sourceDir, targetZip, 'password', {
encryptionMethod: 'AES-256',
compressionLevel: BEST_COMPRESSION,
signal,
})Extract an archive. Optional entries extracts only those paths (directories include nested children).
await unzip(source, target)
await unzip(source, target, 'UTF-8')
await unzip(source, target, ['readme.md', 'docs'])
await unzip(source, target, 'UTF-8', ['readme.md'])
await unzip(source, target, { entries: ['readme.md'], signal })Charset defaults to UTF-8. On iOS, non-UTF-8 values reject with ERR_UNSUPPORTED.
await unzipWithPassword(source, target, 'password')
await unzipWithPassword(source, target, 'password', ['secret.txt'])
await unzipWithPassword(source, target, 'password', { entries: ['secret.txt'], signal })type ZipEntry = {
path: string
size: number // uncompressed bytes
compressedSize: number
isDirectory: boolean
isEncrypted: boolean
}Unzip a bundled archive (relative path only — not an absolute filesystem path).
- Android: path under APK
assets/(also acceptscontent://URIs) - iOS: path in the main app bundle
await unzipAssets('./myFile.zip', DocumentDirectoryPath)
await unzipAssets('./myFile.zip', DocumentDirectoryPath, { signal })Total uncompressed size in bytes. Charset is Android-only; iOS ignores it.
Best-effort abort of the in-flight operation. The active promise rejects with ErrorCodes.CANCELLED (ERR_CANCELLED).
Operations are serialized (Android single-thread executor / iOS serial queue). Concurrent calls queue FIFO; cancel() is not blocked behind in-flight work.
subscribe(({ progress, filePath }) => { /* progress 0…1 */ })- Event is global — match
filePathto your operation, then call.remove(). unzip/unzipWithPassword: byte-weighted after each entry.zip/zipWithPassword: per-file.unzipAssets(Android): approximate vs compressed size.
Stable error.code on both platforms (also on ErrorCodes):
| Code | When |
|---|---|
ERR_FILE_NOT_FOUND |
Source missing |
ERR_INVALID_PATH |
Bad / null path |
ERR_INVALID_ARGS |
Empty password, empty entries, etc. |
ERR_WRONG_PASSWORD |
Decrypt failed |
ERR_NOT_PASSWORD_PROTECTED |
Password API on a plain archive |
ERR_CORRUPT_ARCHIVE |
Not a zip / truncated |
ERR_UNSAFE_PATH |
Zip Slip / path traversal |
ERR_CANCELLED |
cancel() or AbortSignal |
ERR_ZIP / ERR_UNZIP |
Generic failure |
ERR_UNSUPPORTED |
Not available on this platform |
ZipError is a factory (not an ES class). Check error.code; do not use instanceof.
| Feature | iOS | Android |
|---|---|---|
zip / zipWithPassword |
✅ | ✅ |
unzip / unzipWithPassword (+ selective entries) |
✅ | ✅ |
listContents |
✅ | ✅ |
unzipAssets |
✅ | ✅ |
cancel / AbortSignal |
✅ | ✅ |
isPasswordProtected / getUncompressedSize |
✅ | ✅ |
| Progress events | ✅ | ✅ |
Notes
- Encryption: Prefer
'STANDARD'for server-side unzip. AES archives often fail with Nodeunzipper/ JavaZipInputStream. - Charset: Android supports custom charsets; iOS is UTF-8 only (
ERR_UNSUPPORTEDotherwise). - Paths: Decode URL-encoded paths (
decodeURIComponent) before passing them —%20has been mistaken for corrupt archives (#333). - Interop check:
node scripts/validate-zip-header.js /path/to/archive.zipandnpm run test:interop.
Stay on v7 only for RN < 0.70. On 0.70+, install latest v9 and rebuild native — do not stay on v7 for old architecture.
v9 loads via TurboModuleRegistry first, then NativeModules.RNZipArchive. On RN 0.82+, the opt-out flags newArchEnabled=false / RCT_NEW_ARCH_ENABLED=0 are ignored (New Architecture only).
CI compile proof for RN 0.81.6: .github/workflows/old-arch.yml. Agent/contributor details: AGENTS.md.
| App | Stack |
|---|---|
| playground-expo | Expo SDK 55, Expo Router, New Architecture |
| playground-rn | Bare RN 0.83.9, New Architecture |
Both consume the library via file:.. and include Maestro E2E flows under .maestro/.
| This library | JSZip | Nitro unzip/archive | |
|---|---|---|---|
| Zip / unzip | Native iOS + Android | Pure JS | Native via Nitro |
| Password zips | Yes | Small in-memory only | Check those packages |
| Expo Go | No (dev build) | Yes | No (dev build) |
| Extra native deps | None | None | react-native-nitro-modules |
| Large files | Native I/O | Memory-heavy | Varies |
Use this library for on-device native zip/unzip. Use JSZip for small in-JS archives.
npm test # Jest (JS layer + mocks)
npm run test:interop # Node/Java unzip of committed fixtures
npm run test:docs-sync # README ↔ AGENTS.md fact + change pairingE2E (Maestro): see e2e/README.md.
Coming from v7? Start with Upgrade from v7. Full notes: MIGRATION.md.
Supported versions and reporting: SECURITY.md.
- Use the playground apps to exercise changes.
- AGENTS.md — canonical agent guide (agents.md standard). README is for humans; keep shared facts in sync (
npm run test:docs-sync). - Review focus areas: REVIEW.md.
- Optional local gate: pre-commit (
.pre-commit-config.yaml).
Each minor (vX.Y.0) gets one GitHub Discussion in Announcements.
- Add
.github/announcements/vX.Y.mdwith an H1 title and<!-- releases: vX.Y.0 --> - Merge to
master— minor-discussion.yml opens or reuses the Discussion
- ZipArchive (iOS)
- zip4j (Android)
