Handling binary data, file uploads, and secure authentication are standard requirements for modern mobile applications. In web environments, developers frequently utilize Base64 encoding to convert binary data into a safe, ASCII-compatible text format. However, when transitioning from web-based React (React JS) to React Native, developers often run into a wall. Critical browser-native globals like window.btoa() and window.atob() simply do not exist in the React Native JavaScript runtime.
If you have encountered the dreaded "ReferenceError: Can't find variable: btoa" or are struggling to handle large document rendering with React Doc Viewer using Base64, this guide is for you. This comprehensive tutorial will walk you through the inner workings of Base64 encoding and decoding in both web React and React Native environments. You will learn how to bypass standard JS engine limitations, implement high-performance file-to-Base64 conversions, avoid memory leaks, and display Base64 documents natively.
1. The Base64 Dilemma in JavaScript Ecosystems
Base64 is a binary-to-text encoding schema that represents binary data in an ASCII string format. It translates every 3 bytes (24 bits) into four 6-bit characters from a pool of 64 standard characters (A-Z, a-z, 0-9, +, and /), utilizing = as padding when necessary. While it is highly efficient for transferring data over mediums that are designed to handle text, it comes with a trade-off: Base64 encoding increases the overall payload size by approximately 33.3%.
In standard React JS web environments, working with Base64 is remarkably straightforward because the underlying browser provides highly optimized API bindings out of the box:
window.btoa()(Binary to ASCII): Encodes a string of binary data to a Base64 string.window.atob()(ASCII to Binary): Decodes a Base64-encoded string back to raw binary format.
However, React Native is not a browser. It runs JavaScript code inside an independent, lightweight JS engine—traditionally Hermes or JavaScriptCore (JSC)—on the mobile device. This environment represents a clean, bare-bones JS execution context. Because there is no standard HTML DOM or browser host context, global browser objects like window, document, and web-specific APIs are entirely absent. Furthermore, standard Node.js globals like Buffer are also missing.
If a package in your dependency tree calls btoa() or atob(), your React Native application will immediately crash. Understanding how to bridge this gap cleanly is critical to maintaining a fast and reliable application architecture.
2. Mastering Base64 in React JS (Web)
Before diving into mobile-specific solutions, it is essential to understand how to correctly implement "react js base64 encode" and "react base64 decode" in web applications. Many developers incorrectly assume that btoa() and atob() are bulletproof. However, they possess a critical limitation: they only support binary strings encoded with Latin1 (8-bit characters).
If you attempt to encode a string containing Unicode characters (such as emojis or non-English alphabets) using standard btoa(), the browser will throw a DOMException: String contains an invalid character:
// This will crash if standard characters outside Latin1 are used
try {
const rawString = "Hello, World! 🚀";
const encoded = window.btoa(rawString); // Throws DOMException
} catch (error) {
console.error("Encoding failed:", error.message);
}
The Safe UTF-8 Web Solution
To safely perform Base64 operations on the web without crashing on Unicode characters, you must serialize your strings into UTF-8 bytes first. Modern browsers provide the TextEncoder and TextDecoder APIs to solve this exact problem.
Here is how to safely implement UTF-8 compliant Base64 encoding and decoding in a standard React JS application:
// Safe React JS Base64 Encode (UTF-8 Compatible)
export const safeReactJsBase64Encode = (str) => {
const bytes = new TextEncoder().encode(str);
const binString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
return window.btoa(binString);
};
// Safe React JS Base64 Decode (UTF-8 Compatible)
export const safeReactJsBase64Decode = (base64Str) => {
const binString = window.atob(base64Str);
const bytes = Uint8Array.from(binString, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
};
By leveraging this pattern in your web components, you can ensure your React JS web applications remain robust across all multi-language inputs.
3. Implementing React Native Base64: 3 Best Approaches
To achieve consistent, bug-free performance in React Native, you need to rely on alternative tools to perform Base64 operations. Here, we explore the three most common architectural patterns to handle encoding and decoding inside mobile applications.
Approach 1: Lightweight Pure JavaScript Polyfills
If you are handling small payloads (such as API authorization headers or basic status configuration strings) and do not want to link heavy native modules, a pure JavaScript library is the cleanest solution.
The industry standard package for this is base-64 (authored by Mathias Bynens). It is lightweight, reliable, and does not require any platform-specific native build steps.
First, install the package:
npm install base-64
# or
yarn add base-64
Then, import and use the helper functions directly inside your React Native hooks or components:
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet } from 'react-native';
import { encode as btoa, decode as atob } from 'base-64';
export default function Base64Demo() {
const [input, setInput] = useState('');
const [result, setResult] = useState('');
const handleEncode = () => {
const encoded = btoa(input);
setResult(encoded);
};
const handleDecode = () => {
try {
const decoded = atob(input);
setResult(decoded);
} catch (e) {
setResult('Invalid Base64 string');
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Enter text..."
value={input}
onChangeText={setInput}
/>
<View style={styles.row}>
<Button title="Encode" onPress={handleEncode} />
<Button title="Decode" onPress={handleDecode} />
</View>
<Text style={styles.resultText}>Result: {result}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20, marginTop: 50 },
input: { borderWidth: 1, borderColor: '#ccc', padding: 10, marginBottom: 15 },
row: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 20 },
resultText: { fontSize: 16, fontWeight: 'bold', textAlign: 'center' }
});
Setting Up Global Polyfills
Many third-party NPM modules assume that btoa and atob are globally available in the global scope. If you try to run one of these packages, it will crash your application. You can easily fix this by polyfilling the global scope inside your app's root entry point (usually index.js or App.js):
import { decode as atob, encode as btoa } from 'base-64';
if (!global.btoa) {
global.btoa = btoa;
}
if (!global.atob) {
global.atob = atob;
}
Approach 2: Using the Buffer Polyfill
If you are migrating existing React JS business logic or Node.js codebase components over to React Native, using a standard Buffer API is highly advantageous. It provides clean, natively-optimized conversion features that handle binary schemas automatically.
Install the lightweight mobile buffer polyfill:
npm install buffer
Now, utilize the Buffer class inside your files as follows:
import { Buffer } from 'buffer';
// Encoding operation
const rawData = "React Native Base64 Tutorial";
const base64Encoded = Buffer.from(rawData, 'utf-8').toString('base64');
console.log("Encoded Buffer:", base64Encoded);
// Decoding operation
const base64String = "UmVhY3QgTmF0aXZlIEJhc2U2NCBUdXRvcmlhbA==";
const decodedString = Buffer.from(base64String, 'base64').toString('utf-8');
console.log("Decoded Buffer:", decodedString);
This is exceptionally practical for constructing custom cryptographic keys, handling binary WebSockets, or parsing binary stream headers.
Approach 3: High-Performance Native Filesystem Modules
Critical Performance Warning: Pure JavaScript libraries process string arrays character-by-character on the single-threaded React Native JS engine. If you attempt to convert a 10MB PDF document or a high-resolution raw camera photo into a Base64 string using JavaScript-based polyfills, your application will freeze and ultimately crash due to Out of Memory (OOM) exceptions.
For file manipulations, you should completely offload the conversion task from JavaScript to native device threads (Obj-C/Swift for iOS and Java/Kotlin for Android). These platforms perform streaming conversions directly inside the native OS buffer, passing only the final encoded output string back across the React Native bridge.
implementation via Expo FileSystem
If you are building your application inside the Expo ecosystem, use the official expo-file-system module to handle binary conversions safely:
import * as FileSystem from 'expo-file-system';
// Read a local file and instantly retrieve its content as Base64
async function convertFileToBase64(fileUri) {
try {
const base64Data = await FileSystem.readAsStringAsync(fileUri, {
encoding: FileSystem.EncodingType.Base64,
});
return base64Data;
} catch (error) {
console.error("Native encoding failed:", error);
throw error;
}
}
// Write a Base64 string directly back to storage as a binary file
async function saveBase64ToFile(destinationPath, base64Content) {
try {
await FileSystem.writeAsStringAsync(destinationPath, base64Content, {
encoding: FileSystem.EncodingType.Base64,
});
console.log("File written successfully!");
} catch (error) {
console.error("Native file write failed:", error);
}
}
implementation via React Native FS (Bare / CLI workflow)
If you are using a bare React Native CLI workflow, install react-native-fs:
npm install react-native-fs
# Ensure you run 'npx pod-install' on iOS to link native dependencies
Then use the native file system wrappers to stream file data safely:
import RNFS from 'react-native-fs';
// Native Read to Base64
async function readAsBase64Native(filePath) {
try {
const base64String = await RNFS.readFile(filePath, 'base64');
return base64String;
} catch (err) {
console.error("RNFS read error:", err);
}
}
// Native Write Base64 to Local Storage File
async function writeBase64ToFileNative(filePath, base64String) {
try {
await RNFS.writeFile(filePath, base64String, 'base64');
console.log("File stored to disk");
} catch (err) {
console.error("RNFS write error:", err);
}
}
This architecture bypasses the performance limitations of standard JavaScript interpreters, ensuring smooth performance even when handling large media items.
4. Advanced Use Case: React Doc Viewer and Base64 Files
Many web development scenarios require presenting document formats—such as PDFs, Excel spreadsheets, Word files, or system diagnostics—directly within the app interface. In web development, developers frequently rely on the package react-doc-viewer to accomplish this.
To view a base64 string document on the web, you must format the Base64 sequence into a modern Data URI scheme. A typical integration looks like this:
import React from "react";
import DocViewer, { DocViewerRenderers } from "@cyntler/react-doc-viewer";
export default function WebDocPreview({ base64PdfString }) {
// Format base64 string as a compatible Data URI string
const pdfUri = `data:application/pdf;base64,${base64PdfString}`;
const docs = [
{
uri: pdfUri,
fileType: "pdf",
fileName: "invoice_preview.pdf"
}
];
return (
<div style={{ height: "100vh", width: "100%" }}>
<DocViewer
documents={docs}
pluginRenderers={DocViewerRenderers}
config={{
header: {
disableHeader: false,
disableFileName: false,
retainURLParams: false,
},
}}
/>
</div>
);
}
The Mobile Challenge: Viewing Base64 Documents in React Native
Because react-doc-viewer relies on web technologies like standard DOM elements, <iframe> elements, and HTML layouts, it is completely incompatible with mobile React Native apps.
To display a Base64-encoded PDF or file to mobile users, you must download the string, decode it directly onto the disk storage of the mobile device as a local physical file, and render it using mobile-native visual elements.
Here is a complete, step-by-step production pattern to read a Base64-encoded document, write it safely to disk, and present it inside a native mobile system view:
import React, { useState } from 'react';
import { StyleSheet, View, Button, ActivityIndicator, Alert } from 'react-native';
import * as FileSystem from 'expo-file-system';
import * as Sharing from 'expo-sharing';
export default function Base64DocViewer() {
const [isLoading, setIsLoading] = useState(false);
// Dummy Base64 representation of a standard mini-PDF document
const sampleBase64Pdf = "JVBERi0xLjQKJdPr6gogMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nIC9QYWdlcyAyIDAgUiA+PiBlbmRvYmoKMiAwIG9iagogIDw8IC9UeXBlIC9QYWdlcyAvS2lkcyBbIDMgMCBSIF0gL0NvdW50IDEgPj4gZW5kb2JqCjMgMCBvYmoKICA8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9NZWRpYUJveCBbIDAgMCA1OTUgODQyIF0gL1Jlc291cmNlcyA0IDAgUiAvQ29udGVudHMgNSAwIFIgPj4gZW5kb2JqCjQgMCBvYmoKICA8PCAvRm9udCA8PCAvRjEgNiAwIFIgPj4gPj4gZW5kb2JqCjUgMCBvYmoKICA8PCAvTGVuZ3RoIDQ0ID4+IHN0cmVhbQogIEJUIC9GMSAyNCBUZiAxMDAgNzAwIFRkIChIZWxsbyBSZWFjdCBOYXRpdmUgQmFzZTY0ISkgVGogRVQgZW5kc3RyZWFtIGVuZG9iago2IDAgb2JqCiAgPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1R5cGUxIC9CYXNlRm9udCAvSGVsdmV0aWNhID4+IGVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDA2NiAwMDAwMCBuIAowMDAwMDAwMTIxIDAwMDAwIGYgCjAwMDAwMDAyNDAgMDAwMDAgbiAKMDAwMDAwMjg4OSAwMDAwMCBuIAowMDAwMDAwMzY3IDAwMDAwIG4gCnRyYWlsZXIKICA8PCAvU2l6ZSA3IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgogNDUwCiUlRU9GCg==";
const handleOpenDoc = async () => {
setIsLoading(true);
try {
const fileName = "Invoice_Download.pdf";
// Define location inside private App Cache directory to prevent bloating user disk space permanently
const fileUri = `${FileSystem.cacheDirectory}${fileName}`;
// 1. Write the Base64 string directly to the device storage
await FileSystem.writeAsStringAsync(fileUri, sampleBase64Pdf, {
encoding: FileSystem.EncodingType.Base64,
});
// 2. Check if file sharing or system-level document viewing is supported
const isAvailable = await Sharing.isAvailableAsync();
if (isAvailable) {
// Launch standard system sharing view to let user open in PDF Reader, mail, etc.
await Sharing.shareAsync(fileUri);
} else {
Alert.alert("Error", "System sharing is not available on this device.");
}
} catch (error) {
console.error("Error displaying document:", error);
Alert.alert("Error", "Could not load document preview");
} finally {
setIsLoading(false);
}
};
return (
<View style={styles.center}>
{isLoading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : (
<Button title="Open Base64 PDF Document" onPress={handleOpenDoc} />
)}
</View>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }
});
This pattern bypasses complex UI limitations, giving users a direct, secure, and native viewing experience across iOS and Android without third-party rendering issues.
5. Performance Optimization & Production Best Practices
When deploying Base64 pipelines inside actual production codebases, performance bugs can severely impact app performance and user retention. Follow these optimization guidelines to keep your React Native application running at peak efficiency:
1. Shift Large Operations Off the JavaScript Thread
As discussed previously, running heavy loop processing inside JavaScript blocks the single-threaded rendering loop. If you are converting images, camera footage, or user documents, always utilize native platforms such as react-native-fs or expo-file-system. Avoid executing large data mappings in pure JS helper files.
2. Bypass Base64 Uploads via Multipart Form Uploads
Many developers blindly convert files to Base64 to transmit them to a backend database or REST API. However, this increases user internet bandwidth usage by roughly 33.3%, which degrades user experience on slow mobile connections.
Instead, use standard Multipart FormData file transfers. This method streams raw bytes directly from the storage path to the server without any encoding overhead:
const uploadMediaFile = async (localFileUri) => {
const formData = new FormData();
formData.append('file', {
uri: localFileUri,
name: 'uploaded_image.jpg',
type: 'image/jpeg',
});
const response = await fetch('https://api.yourdomain.com/upload', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
return await response.json();
};
3. Implement Garbage Collection Cleanups
Because native file buffers passed into your JS code create massive text string variables, those strings can linger in memory and cause significant bloat. Once you finish performing an operation with a heavy Base64 string, set its variable references back to null to notify the runtime JavaScript Garbage Collector to immediately reclaim that space:
let heavyData = await RNFS.readFile(path, 'base64');
await sendDataToServer(heavyData);
heavyData = null; // Mark variable reference for immediate garbage collection cleanup
6. Frequently Asked Questions (FAQ)
Why does btoa or atob throw "ReferenceError: Can't find variable" in React Native?
React Native does not run your code within a web browser, and therefore lack access to the browser's native window runtime object. Since btoa and atob are legacy host APIs bound specifically to the DOM window context, the React Native environment throws an error because they do not exist globally. This can be easily resolved by applying standard JS libraries like base-64 or importing Buffer polyfills.
How can I render a Base64-encoded image in React Native?
You can render Base64 images inside React Native without any external libraries. Pass the properly formatted standard Data URI string directly into the React Native <Image> element source parameter:
import React from 'react';
import { Image } from 'react-native';
export default function Base64ImageDemo() {
const base64Uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
return (
<Image
source={{ uri: base64Uri }}
style={{ width: 100, height: 100, borderRadius: 8 }}
/>
);
}
What is the safest way to convert non-English characters or emojis in React Native Base64?
Pure JS encoding helpers can crash when parsing Unicode characters. The safest approach in React Native is to implement the lightweight, standard Node.js Buffer polyfill. Running Buffer.from(rawStr, 'utf-8').toString('base64') completely handles emojis, special foreign languages, and non-standard UTF-8 symbols cleanly.
Can I use Expo FileSystem outside of the Managed Expo Workflow?
Yes. Under the unified Expo modules system, you can safely install expo-file-system inside any bare React Native CLI project by adding the standard Expo installation library packages.
Conclusion
Working with Base64 in modern JavaScript requires adjusting your approach based on the execution environment. In React JS, standard browser APIs combined with correct UTF-8 conversions will keep your applications robust and performant. In React Native, developers must respect the single-threaded mobile bridge architecture.
For basic string manipulations, lightweight polyfills or standard Buffer imports work beautifully. However, when working with large files, documents, or photos, always use native filesystem solutions to maintain a responsive user interface. Armed with these techniques, you can confidently integrate robust encoding, decoding, and document rendering pipelines into your production applications.






