Sheets on the Go with React Native
React Native is a mobile app framework. It builds iOS and Android apps that use JavaScript for describing layouts and events.
SheetJS is a JavaScript library for reading and writing data from spreadsheets.
This demo uses React Native and SheetJS to process and generate spreadsheets. We'll explore how to load SheetJS in a React Native app in a few ways:
- "Fetching Remote Data" uses the built-in
fetch
to download and parse remote workbook files. - "Local Files" uses native libraries to read and write files on the device.
The "Local Files" example creates an app that looks like the screenshots below:
iOS | Android |
---|---|
Before testing this demo, follow the official React Native CLI Guide!1
Follow the instructions for iOS (requires macOS) and for Android. They will cover installation and system configuration. You should be able to build and run a sample app in the Android and the iOS (if applicable) simulators.
Integration Details
The SheetJS NodeJS Module can be imported from any component or script in the app.
Internal State
For simplicity, this demo uses an "Array of Arrays"2 as the internal state.
Spreadsheet | Array of Arrays |
---|---|
|
Each array within the structure corresponds to one row.
This demo also keeps track of the column widths as a single array of numbers. The widths are used by the display component.
Complete State
The complete state is initialized with the following snippet:
const [data, setData] = useState([
"SheetJS".split(""),
[5,4,3,3,7,9,5],
[8,6,7,5,3,0,9]
]);
const [widths, setWidths] = useState(Array.from({length:7}, () => 20));
Updating State
Starting from a SheetJS worksheet object, sheet_to_json
3 with the header
option can generate an array of arrays:
/* assuming `wb` is a SheetJS workbook */
function update_state(wb) {
/* convert first worksheet to AOA */
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const data = utils.sheet_to_json(ws, {header:1});
/* update state */
setData(data);
/* update column widths */
setWidths(make_width(data));
}
Calculating Column Widths
Column widths can be calculated by walking each column and calculating the max data width. Using the array of arrays:
/* this function takes an array of arrays and generates widths */
function make_width(aoa) {
/* walk each row */
aoa.forEach((r) => {
/* walk each column */
r.forEach((c, C) => {
/* update column width based on the length of the cell contents */
res[C] = Math.max(res[C]||60, String(c).length * 10);
});
});
/* use a default value for columns with no data */
for(let C = 0; C < res.length; ++C) if(!res[C]) res[C] = 60;
return res;
}
Exporting State
aoa_to_sheet
4 builds a SheetJS worksheet object from the array of arrays:
/* generate a SheetJS workbook from the state */
function export_state() {
/* convert AOA back to worksheet */
const ws = utils.aoa_to_sheet(data);
/* build new workbook */
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "SheetJS");
return wb;
}
Displaying Data
The demos uses react-native-table-component
to display the first worksheet.
The demos use components similar to the example below:
import { ScrollView } from 'react-native';
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
(
{/* Horizontal scroll */}
<ScrollView horizontal={true} >
{/* Table container */}
<Table>
{/* Frozen Header Row */}
<TableWrapper>
{/* First row */}
<Row data={data[0]} widthArr={widths}/>
</TableWrapper>
{/* Scrollable Data Rows */}
<ScrollView>
<TableWrapper>
{/* Remaining Rows */}
<Rows data={data.slice(1)} widthArr={widths}/>
</TableWrapper>
</ScrollView>
</Table>
</ScrollView>
)
data.slice(1)
in the Rows
component returns data starting from the second
row. This neatly skips the first header row.
Fetching Remote Data
React Native versions starting from 0.72.0
5 support binary data with fetch
.
This snippet downloads and parses https://sheetjs.com/pres.xlsx:
/* fetch data into an ArrayBuffer */
const ab = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
/* parse data */
const wb = XLSX.read(ab);
Fetch Demo
This demo was tested in the following environments:
OS | Type | Device | RN | Date |
---|---|---|---|---|
Android 34 | Sim | Pixel 3a | 0.73.1 | 2023-12-21 |
iOS 17.2 | Sim | iPhone 15 Pro Max | 0.73.1 | 2023-12-21 |
Android 29 | Real | NVIDIA Shield | 0.73.1 | 2023-12-21 |
iOS 15.1 | Real | iPad Pro | 0.73.1 | 2023-12-21 |
1) Create project:
npx -y [email protected] init SheetJSRNFetch --version="0.73.1"
2) Install shared dependencies:
cd SheetJSRNFetch
curl -LO https://docs.sheetjs.com/logo.png
npm i -S https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz
npm i -S [email protected] @types/react-native-table-component
3) Download App.tsx
and replace:
curl -LO https://docs.sheetjs.com/reactnative/App.tsx
Android Testing
4) Install or switch to Java 176
When the demo was last tested on macOS, java -version
displayed the following:
openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment Temurin-17.0.9+9 (build 17.0.9+9)
OpenJDK 64-Bit Server VM Temurin-17.0.9+9 (build 17.0.9+9, mixed mode)
5) Start the Android emulator:
npx react-native run-android
If the initial launch fails with an error referencing the emulator, manually start the emulator and try again.
Gradle errors typically stem from a Java version mismatch:
> Failed to apply plugin 'com.android.internal.application'.
> Android Gradle plugin requires Java 17 to run. You are currently using Java 11.
This error can be resolved by installing and switching to the requested version.
6) When opened, the app should look like the "Before" screenshot below. After tapping "Import data from a spreadsheet", verify that the app shows new data:
Before | After |
---|---|
iOS Testing
iOS testing can only be performed on Apple hardware running macOS!
Xcode and iOS simulators are not available on Windows or Linux.
7) Refresh iOS project by running pod install
from the ios
subfolder:
cd ios; pod install; cd ..
8) Start the iOS emulator:
npx react-native run-ios
9) When opened, the app should look like the "Before" screenshot below. After tapping "Import data from a spreadsheet", verify that the app shows new data:
Before | After |
---|---|
Android Device Testing
10) Connect an Android device using a USB cable.
If the device asks to allow USB debugging, tap "Allow".
11) Close any Android / iOS emulators.
12) Build APK and run on device:
npx react-native run-android
iOS Device Testing
13) Close any Android / iOS emulators.
14) Enable developer code signing certificates7.
15) Install ios-deploy
through Homebrew:
brew install ios-deploy
16) Run on device:
npx react-native run-ios
When this demo was last tested, the build failed with the following error:
PhaseScriptExecution failed with a nonzero exit code
This was due to an error in the react-native
package. The script
node_modules/react-native/scripts/react-native-xcode.sh
must be edited.
Near the top of the script, there will be a set
statement:
# Print commands before executing them (useful for troubleshooting)
set -x -e
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH
The -e
argument must be removed:
# Print commands before executing them (useful for troubleshooting)
set -x
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH
By default, React Native generates applications that exclusively target iPhone. On a physical iPad, a pixellated iPhone app will be run.
The "targeted device families" setting must be changed to support iPad:
A) Open the Xcode workspace:
open ./ios/SheetJSRNFetch.xcworkspace
B) Select the project in the left sidebar:
C) Select the "SheetJSRNFetch" target in the sidebar.
D) Select the "Build Settings" tab in the main area.
E) In the search bar below "Build Settings", type "tar"
F) Look for the "Targeted Device Families" row. Change the corresponding value to "iPhone, iPad".
Local Files
React Native does not provide a native file picker or a method for reading and writing data from documents on the devices. A third-party library must be used.
Since React Native internals change between releases, libraries may only work with specific versions of React Native. Project documentation should be consulted before picking a library.
The following table lists tested file plugins. "OS" lists tested platforms ("A" for Android and "I" for iOS).
File system Plugin | File Picker Plugin | OS |
---|---|---|
react-native-file-access | react-native-document-picker | AI |
react-native-blob-util | react-native-document-picker | AI |
rn-fetch-blob | react-native-document-picker | AI |
react-native-fs | react-native-document-picker | AI |
expo-file-system | expo-document-picker | I |
RN File Picker
The "File Picker" library handles two platform-specific steps:
1) Show a view that allows users to select a file from their device
2) Copy the selected file to a location that can be read by the application
The following libraries have been tested:
react-native-document-picker
Selecting a file (click to hide)
The setting copyTo: "cachesDirectory"
must be set:
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({
allowMultiSelection: false,
copyTo: "cachesDirectory",
mode: "open"
});
const path = f.fileCopyUri; // this path can be read by RN file plugins
expo-document-picker
Selecting a file (click to show)
When using DocumentPicker.getDocumentAsync
, enable copyToCacheDirectory
:
import * as DocumentPicker from 'expo-document-picker';
const result = await DocumentPicker.getDocumentAsync({
copyToCacheDirectory: true,
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
});
const path = result.uri; // this path can be read by RN file plugins
RN File Plugins
The following libraries have been tested:
react-native-blob-util
and rn-fetch-blob
The react-native-fetch-blob
project was archived in 2019. At the time, there
were a number of project forks. The maintainers blessed the rn-fetch-blob
fork as the spiritual successor.
react-native-blob-util
is an active fork of rn-fetch-blob
When this demo was last tested, rn-fetch-blob
and react-native-blob-util
both worked with the tested iOS and Android SDK versions. The APIs are identical
for the purposes of working with files.
The ascii
type returns an array of numbers corresponding to the raw bytes.
A Uint8Array
from the data is compatible with the buffer
type.
Reading and Writing snippets (click to hide)
The snippets use rn-fetch-blob
. To use react-native-blob-util
, change the
import
statements to load the module.
Reading Data
import * as XLSX from "xlsx";
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { readFile } = RNFetchBlob.fs;
const res = await readFile(path, 'ascii');
const wb = XLSX.read(new Uint8Array(res), {type:'buffer'});
On iOS, the URI from react-native-document-picker
must be massaged:
import { pickSingle } from 'react-native-document-picker';
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs;
const f = await pickSingle({
// Instruct the document picker to copy file to Documents directory
copyTo: "documentDirectory",
allowMultiSelection: false, mode: "open" });
// `f.uri` is the original path and `f.fileCopyUri` is the path to the copy
let path = f.fileCopyUri;
// iOS workaround
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/");
const res = await readFile(path, 'ascii');
Writing Data
import * as XLSX from "xlsx";
import RNFetchBlob from 'rn-fetch-blob'; // or react-native-blob-util
const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs;
const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"});
const file = DocumentDir + "/sheetjsw.xlsx";
const res = await writeFile(file, Array.from(wbout), 'ascii');
react-native-file-access
The base64
encoding returns strings compatible with the base64
type:
Reading and Writing snippets (click to hide)
Reading Data
import * as XLSX from "xlsx";
import { FileSystem } from "react-native-file-access";
const b64 = await FileSystem.readFile(path, "base64");
/* b64 is a Base64 string */
const workbook = XLSX.read(b64, {type: "base64"});
Writing Data
import * as XLSX from "xlsx";
import { Dirs, FileSystem } from "react-native-file-access";
const DDP = Dirs.DocumentDir + "/";
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64");
react-native-fs
The ascii
encoding returns binary strings compatible with the binary
type:
Reading and Writing snippets (click to hide)
Reading Data
import * as XLSX from "xlsx";
import { readFile } from "react-native-fs";
const bstr = await readFile(path, "ascii");
/* bstr is a binary string */
const workbook = XLSX.read(bstr, {type: "binary"});
Writing Data
import * as XLSX from "xlsx";
import { writeFile, DocumentDirectoryPath } from "react-native-fs";
const bstr = XLSX.write(workbook, {type:'binary', bookType:"xlsx"});
/* bstr is a binary string */
await writeFile(DocumentDirectoryPath + "/sheetjs.xlsx", bstr, "ascii");
expo-file-system
Some Expo APIs return URI that cannot be read with expo-file-system
. This
will manifest as an error:
Unsupported scheme for location '...'
The expo-document-picker
snippet makes a local copy.
The EncodingType.Base64
encoding is compatible with base64
type.
Reading and Writing snippets (click to show)
Reading Data
Calling FileSystem.readAsStringAsync
with FileSystem.EncodingType.Base64
encoding returns a promise resolving to a string compatible with base64
type:
import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
const workbook = XLSX.read(b64, { type: "base64" });
Writing Data
The FileSystem.EncodingType.Base64
encoding accepts Base64 strings:
import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 });
Demo
Each Android demo was last tested on 2023 September 03 with RN 0.72.6
. The
simulator used Android 13 ("Tiramisu") API 33 on a Pixel 3.
Each iOS demo was last tested on 2023 September 03 with RN 0.72.6
. The
simulator used iOS 17.0 on an iPhone 15 Pro Max.
There are many moving parts and pitfalls with React Native apps. It is strongly recommended to follow the official React Native tutorials for iOS and Android before approaching this demo.8 Details including Android Virtual Device configuration are not covered here.
This example tries to separate the library-specific functions.
1) Create project:
npx react-native init SheetJSRN --version="0.72.6"
2) Install shared dependencies:
cd SheetJSRN
curl -LO https://docs.sheetjs.com/logo.png
npm i -S https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz
npm i -S [email protected] [email protected]
Refresh iOS project by running pod install
from the ios
subfolder:
cd ios
pod install
cd ..
3) Download index.js
and replace:
curl -LO https://docs.sheetjs.com/mobile/index.js
Start the iOS emulator:
npx react-native run-ios
You should see the skeleton app:
4) Pick a filesystem library for integration:
- RNBU
- RNFA
- RNFB
- RNFS
- EXPO
Install react-native-blob-util
dependency:
npm i -S [email protected]
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
async function pickAndParse() {
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await (await fetch(path)).arrayBuffer(); // RN >= 0.72
// const res = await RNFetchBlob.fs.readFile(path, 'ascii'); // RN < 0.72
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
return file;
}
const make_width = ws => {
Install react-native-file-access
dependency:
npm i -S [email protected]
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import { Dirs, FileSystem } from 'react-native-file-access';
async function pickAndParse() {
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
const res = await (await fetch(path)).arrayBuffer();
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
const file = Dirs.DocumentDir + "/sheetjsw.xlsx";
await FileSystem.writeFile(file, wbout, "base64");
return file;
}
const make_width = ws => {
Install rn-fetch-blob
dependency:
npm i -S [email protected]
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
async function pickAndParse() {
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await (await fetch(path)).arrayBuffer(); // RN >= 0.72
// const res = await RNFetchBlob.fs.readFile(path, 'ascii'); // RN < 0.72
return read(new Uint8Array(res), {type: 'buffer'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
return file;
}
const make_width = ws => {
Install react-native-fs
dependency:
npm i -S [email protected]
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs';
async function pickAndParse() {
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
const bstr = await readFile(f.fileCopyUri, 'ascii');
return read(bstr, {type:'binary'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'binary', bookType:"xlsx"});
const file = DocumentDirectoryPath + "/sheetjsw.xlsx";
await writeFile(file, wbout, 'ascii');
return file;
}
const make_width = ws => {
At the time of testing, Expo Modules were incompatible with Android projects.
Install expo-file-system
and expo-document-picker
dependencies:
npx install-expo-modules
npm i -S expo-file-system expo-document-picker
In the most recent test, the installation asked a few questions.
If prompted to change iOS deployment target, choose Yes.
If prompted to install Expo CLI integration, choose No.
Add the highlighted lines to index.js
:
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
import { read, write } from 'xlsx';
import { getDocumentAsync } from 'expo-document-picker';
import { documentDirectory, readAsStringAsync, writeAsStringAsync } from 'expo-file-system';
async function pickAndParse() {
const result = await getDocumentAsync({copyToCacheDirectory: true});
const path = result.uri;
const res = await readAsStringAsync(path, { encoding: "base64" });
return read(res, {type: 'base64'});
}
async function writeWorkbook(wb) {
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
const file = documentDirectory + "sheetjsw.xlsx";
await writeAsStringAsync(file, wbout, { encoding: "base64" });
return file;
}
const make_width = ws => {
5) Refresh the app:
cd ios
pod install
cd ..
Once refreshed, the development process must be restarted:
npx react-native run-ios
iOS Testing
The app can be tested with the following sequence in the simulator:
- Download https://sheetjs.com/pres.numbers
- In the simulator, click the Home icon to return to the home screen
- Click on the "Files" icon
- Click and drag
pres.numbers
from a Finder window into the simulator.
- Make sure "On My iPhone" is highlighted and select "Save"
- Click the Home icon again then select the
SheetJSRN
app - Click "Import data" and select
pres
:
Once selected, the screen should refresh with new contents:
- Click "Export data". You will see a popup with a location:
- Find the file and verify the contents are correct:
find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done
Once testing is complete, stop the simulator and the development process.
Android Testing
There are no Android-specific steps. Emulator can be started with:
npx react-native run-android
The app can be tested with the following sequence in the simulator:
- Download https://sheetjs.com/pres.numbers
- Click and drag
pres.numbers
from a Finder window into the simulator. - Click "Import data" and select
pres.numbers
:
Once selected, the screen should refresh with new contents:
- Click "Export data". You will see a popup with a location:
- Pull the file from the simulator and verify the contents:
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
npx xlsx-cli /tmp/sheetjsw.xlsx
- Follow the "React Native CLI Quickstart" and select the appropriate "Development OS".↩
- See "Array of Arrays" in the API reference↩
- See "Array Output" in "Utility Functions"↩
- See "Array of Arrays Input" in "Utility Functions"↩
- React-Native commit
5b597b5
added the final piece required forfetch
support. It landed in version0.72.0-rc.1
and is available in official releases starting from0.72.0
.↩ - When the demo was last tested, the Temurin distribution of Java 17 was installed through the macOS Brew package manager by running
brew install temurin17
. Direct downloads are available atadoptium.net
↩ - See "Running On Device" in the React Native documentation↩
- Follow the "React Native CLI Quickstart" for Android (and iOS, if applicable)↩