992 lines
26 KiB
Markdown
992 lines
26 KiB
Markdown
|
---
|
||
|
title: React Native
|
||
|
sidebar_label: React Native
|
||
|
description: Build data-intensive mobile apps with React Native. Seamlessly integrate spreadsheets into your app using SheetJS. Securely process and generate Excel files in the field.
|
||
|
pagination_prev: demos/static/index
|
||
|
pagination_next: demos/desktop/index
|
||
|
sidebar_position: 1
|
||
|
sidebar_custom_props:
|
||
|
summary: React + Native Rendering
|
||
|
---
|
||
|
|
||
|
# Sheets on the Go with React Native
|
||
|
|
||
|
import current from '/version.js';
|
||
|
import Tabs from '@theme/Tabs';
|
||
|
import TabItem from '@theme/TabItem';
|
||
|
import CodeBlock from '@theme/CodeBlock';
|
||
|
|
||
|
[React Native](https://reactnative.dev/) is a mobile app framework. It builds
|
||
|
iOS and Android apps that use JavaScript for describing layouts and events.
|
||
|
|
||
|
[SheetJS](https://sheetjs.com) 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"](#fetching-remote-data) uses the built-in `fetch` to
|
||
|
download and parse remote workbook files.
|
||
|
- ["Local 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:
|
||
|
|
||
|
<table><thead><tr>
|
||
|
<th><a href="#demo">iOS</a></th>
|
||
|
<th><a href="#demo">Android</a></th>
|
||
|
</tr></thead><tbody><tr><td>
|
||
|
|
||
|
![iOS screenshot](pathname:///mobile/rnios3.png)
|
||
|
|
||
|
</td><td>
|
||
|
|
||
|
![Android screenshot](pathname:///mobile/rnand3.png)
|
||
|
|
||
|
</td></tr></tbody></table>
|
||
|
|
||
|
:::caution pass
|
||
|
|
||
|
**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](/docs/getting-started/installation/nodejs) 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.
|
||
|
|
||
|
<table><thead><tr><th>Spreadsheet</th><th>Array of Arrays</th></tr></thead><tbody><tr><td>
|
||
|
|
||
|
![`pres.xlsx` data](pathname:///pres.png)
|
||
|
|
||
|
</td><td>
|
||
|
|
||
|
```js
|
||
|
[
|
||
|
["Name", "Index"],
|
||
|
["Bill Clinton", 42],
|
||
|
["GeorgeW Bush", 43],
|
||
|
["Barack Obama", 44],
|
||
|
["Donald Trump", 45],
|
||
|
["Joseph Biden", 46]
|
||
|
]
|
||
|
```
|
||
|
|
||
|
</td></tr></tbody></table>
|
||
|
|
||
|
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:
|
||
|
|
||
|
```js
|
||
|
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:
|
||
|
|
||
|
```js
|
||
|
/* 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:
|
||
|
|
||
|
```js
|
||
|
/* 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:
|
||
|
|
||
|
```js
|
||
|
/* 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:
|
||
|
|
||
|
```jsx
|
||
|
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>:
|
||
|
|
||
|
```js
|
||
|
/* 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
|
||
|
|
||
|
:::note Tested Deployments
|
||
|
|
||
|
This demo was tested in the following environments:
|
||
|
|
||
|
| OS | Type | Device | RN | Date |
|
||
|
|:-----------|:-----|:--------------------|:---------|:-----------|
|
||
|
| Android 34 | Sim | Pixel 3a | `0.72.7` | 2023-12-04 |
|
||
|
| iOS 17.0.1 | Sim | iPhone 15 Pro Max | `0.72.7` | 2023-12-04 |
|
||
|
| Android 29 | Real | NVIDIA Shield | `0.72.7` | 2023-12-04 |
|
||
|
| iOS 15.1 | Real | iPad Pro | `0.72.7` | 2023-12-04 |
|
||
|
|
||
|
:::
|
||
|
|
||
|
1) Create project:
|
||
|
|
||
|
```bash
|
||
|
npx -y react-native@0.72.7 init SheetJSRNFetch --version="0.72.7"
|
||
|
```
|
||
|
|
||
|
2) Install shared dependencies:
|
||
|
|
||
|
<CodeBlock language="bash">{`\
|
||
|
cd SheetJSRNFetch
|
||
|
curl -LO https://docs.sheetjs.com/logo.png
|
||
|
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
|
||
|
npm i -S react-native-table-component@1.2.0 @types/react-native-table-component`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
3) Download [`App.tsx`](pathname:///reactnative/App.tsx) and replace:
|
||
|
|
||
|
```bash
|
||
|
curl -LO https://docs.sheetjs.com/reactnative/App.tsx
|
||
|
```
|
||
|
|
||
|
**Android Testing**
|
||
|
|
||
|
4) Install or switch to Java 11[^6]
|
||
|
|
||
|
:::note pass
|
||
|
|
||
|
When the demo was last tested on macOS, `java -version` displayed the following:
|
||
|
|
||
|
```
|
||
|
openjdk version "11.0.21" 2023-10-17 LTS
|
||
|
OpenJDK Runtime Environment Zulu11.68+17-CA (build 11.0.21+9-LTS)
|
||
|
OpenJDK 64-Bit Server VM Zulu11.68+17-CA (build 11.0.21+9-LTS, mixed mode)
|
||
|
```
|
||
|
|
||
|
:::
|
||
|
|
||
|
5) Start the Android emulator:
|
||
|
|
||
|
```bash
|
||
|
npx react-native run-android
|
||
|
```
|
||
|
|
||
|
:::caution pass
|
||
|
|
||
|
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. Run `java -version`
|
||
|
and verify that the Java major version is 11.
|
||
|
|
||
|
:::
|
||
|
|
||
|
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:
|
||
|
|
||
|
<table><thead><tr>
|
||
|
<th>Before</th>
|
||
|
<th>After</th>
|
||
|
</tr></thead><tbody><tr><td>
|
||
|
|
||
|
![before screenshot](pathname:///reactnative/andfetch1.png)
|
||
|
|
||
|
</td><td>
|
||
|
|
||
|
![after screenshot](pathname:///reactnative/andfetch2.png)
|
||
|
|
||
|
</td></tr></tbody></table>
|
||
|
|
||
|
**iOS Testing**
|
||
|
|
||
|
:::warning pass
|
||
|
|
||
|
iOS testing requires macOS. It does not work on Windows or Linux.
|
||
|
|
||
|
:::
|
||
|
|
||
|
7) Refresh iOS project by running `pod install` from the `ios` subfolder:
|
||
|
|
||
|
```bash
|
||
|
cd ios; pod install; cd ..
|
||
|
```
|
||
|
|
||
|
8) Start the iOS emulator:
|
||
|
|
||
|
```bash
|
||
|
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:
|
||
|
|
||
|
<table><thead><tr>
|
||
|
<th>Before</th>
|
||
|
<th>After</th>
|
||
|
</tr></thead><tbody><tr><td>
|
||
|
|
||
|
![before screenshot](pathname:///reactnative/iosfetch1.png)
|
||
|
|
||
|
</td><td>
|
||
|
|
||
|
![after screenshot](pathname:///reactnative/iosfetch2.png)
|
||
|
|
||
|
</td></tr></tbody></table>
|
||
|
|
||
|
**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:
|
||
|
|
||
|
```bash
|
||
|
npx react-native run-android
|
||
|
```
|
||
|
|
||
|
**iOS Device Testing**
|
||
|
|
||
|
13) Close any Android / iOS emulators.
|
||
|
|
||
|
14) Enable developer code signing certificates[^7]
|
||
|
|
||
|
15) Install `ios-deploy` through Homebrew:
|
||
|
|
||
|
```bash
|
||
|
brew install ios-deploy
|
||
|
```
|
||
|
|
||
|
16) Run on device:
|
||
|
|
||
|
```bash
|
||
|
npx react-native run-ios
|
||
|
```
|
||
|
|
||
|
## Local Files
|
||
|
|
||
|
:::warning pass
|
||
|
|
||
|
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`
|
||
|
|
||
|
<details open><summary><b>Selecting a file</b> (click to hide)</summary>
|
||
|
|
||
|
The setting `copyTo: "cachesDirectory"` must be set:
|
||
|
|
||
|
```js
|
||
|
import { pickSingle } from 'react-native-document-picker';
|
||
|
|
||
|
const f = await pickSingle({
|
||
|
allowMultiSelection: false,
|
||
|
// highlight-next-line
|
||
|
copyTo: "cachesDirectory",
|
||
|
mode: "open"
|
||
|
});
|
||
|
const path = f.fileCopyUri; // this path can be read by RN file plugins
|
||
|
```
|
||
|
|
||
|
</details>
|
||
|
|
||
|
#### `expo-document-picker`
|
||
|
|
||
|
<details><summary><b>Selecting a file</b> (click to show)</summary>
|
||
|
|
||
|
When using `DocumentPicker.getDocumentAsync`, enable `copyToCacheDirectory`:
|
||
|
|
||
|
```js
|
||
|
import * as DocumentPicker from 'expo-document-picker';
|
||
|
|
||
|
const result = await DocumentPicker.getDocumentAsync({
|
||
|
// highlight-next-line
|
||
|
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
|
||
|
```
|
||
|
|
||
|
</details>
|
||
|
|
||
|
|
||
|
### RN File Plugins
|
||
|
|
||
|
The following libraries have been tested:
|
||
|
|
||
|
#### `react-native-blob-util` and `rn-fetch-blob`
|
||
|
|
||
|
:::note Historical Context
|
||
|
|
||
|
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.
|
||
|
|
||
|
<details open><summary><b>Reading and Writing snippets</b> (click to hide)</summary>
|
||
|
|
||
|
The snippets use `rn-fetch-blob`. To use `react-native-blob-util`, change the
|
||
|
`import` statements to load the module.
|
||
|
|
||
|
_Reading Data_
|
||
|
|
||
|
```js
|
||
|
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'});
|
||
|
```
|
||
|
|
||
|
:::caution pass
|
||
|
|
||
|
On iOS, the URI from `react-native-document-picker` must be massaged:
|
||
|
|
||
|
```js
|
||
|
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({
|
||
|
// highlight-start
|
||
|
// Instruct the document picker to copy file to Documents directory
|
||
|
copyTo: "documentDirectory",
|
||
|
// highlight-end
|
||
|
allowMultiSelection: false, mode: "open" });
|
||
|
// highlight-start
|
||
|
// `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 + "/");
|
||
|
// highlight-end
|
||
|
|
||
|
const res = await readFile(path, 'ascii');
|
||
|
```
|
||
|
|
||
|
:::
|
||
|
|
||
|
_Writing Data_
|
||
|
|
||
|
```js
|
||
|
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');
|
||
|
```
|
||
|
|
||
|
|
||
|
</details>
|
||
|
|
||
|
#### `react-native-file-access`
|
||
|
|
||
|
The `base64` encoding returns strings compatible with the `base64` type:
|
||
|
|
||
|
<details open><summary><b>Reading and Writing snippets</b> (click to hide)</summary>
|
||
|
|
||
|
_Reading Data_
|
||
|
|
||
|
```js
|
||
|
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_
|
||
|
|
||
|
```js
|
||
|
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");
|
||
|
```
|
||
|
|
||
|
</details>
|
||
|
|
||
|
#### `react-native-fs`
|
||
|
|
||
|
The `ascii` encoding returns binary strings compatible with the `binary` type:
|
||
|
|
||
|
<details open><summary><b>Reading and Writing snippets</b> (click to hide)</summary>
|
||
|
|
||
|
_Reading Data_
|
||
|
|
||
|
```js
|
||
|
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_
|
||
|
|
||
|
```js
|
||
|
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");
|
||
|
```
|
||
|
|
||
|
</details>
|
||
|
|
||
|
#### `expo-file-system`
|
||
|
|
||
|
:::caution pass
|
||
|
|
||
|
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`](#expo-document-picker) snippet makes a local copy.
|
||
|
|
||
|
:::
|
||
|
|
||
|
The `EncodingType.Base64` encoding is compatible with `base64` type.
|
||
|
|
||
|
<details><summary><b>Reading and Writing snippets</b> (click to show)</summary>
|
||
|
|
||
|
_Reading Data_
|
||
|
|
||
|
Calling `FileSystem.readAsStringAsync` with `FileSystem.EncodingType.Base64`
|
||
|
encoding returns a promise resolving to a string compatible with `base64` type:
|
||
|
|
||
|
```js
|
||
|
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:
|
||
|
|
||
|
```js
|
||
|
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 });
|
||
|
```
|
||
|
|
||
|
</details>
|
||
|
|
||
|
### Demo
|
||
|
|
||
|
:::note Tested Deployments
|
||
|
|
||
|
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.
|
||
|
|
||
|
:::
|
||
|
|
||
|
:::warning pass
|
||
|
|
||
|
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:
|
||
|
|
||
|
```bash
|
||
|
npx react-native init SheetJSRN --version="0.72.6"
|
||
|
```
|
||
|
|
||
|
2) Install shared dependencies:
|
||
|
|
||
|
<CodeBlock language="bash">{`\
|
||
|
cd SheetJSRN
|
||
|
curl -LO https://docs.sheetjs.com/logo.png
|
||
|
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
|
||
|
npm i -S react-native-table-component@1.2.0 react-native-document-picker@8.2.0`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
Refresh iOS project by running `pod install` from the `ios` subfolder:
|
||
|
|
||
|
```bash
|
||
|
cd ios
|
||
|
pod install
|
||
|
cd ..
|
||
|
```
|
||
|
|
||
|
3) Download [`index.js`](pathname:///mobile/index.js) and replace:
|
||
|
|
||
|
```bash
|
||
|
curl -LO https://docs.sheetjs.com/mobile/index.js
|
||
|
```
|
||
|
|
||
|
Start the iOS emulator:
|
||
|
|
||
|
```bash
|
||
|
npx react-native run-ios
|
||
|
```
|
||
|
|
||
|
You should see the skeleton app:
|
||
|
|
||
|
![React Native iOS App](pathname:///reactnative/ios1.png)
|
||
|
|
||
|
4) Pick a filesystem library for integration:
|
||
|
|
||
|
|
||
|
<Tabs>
|
||
|
<TabItem value="RNBU" label="RNBU">
|
||
|
|
||
|
Install `react-native-blob-util` dependency:
|
||
|
|
||
|
```bash
|
||
|
npm i -S react-native-blob-util@0.19.2
|
||
|
```
|
||
|
|
||
|
Add the highlighted lines to `index.js`:
|
||
|
|
||
|
```js title="index.js"
|
||
|
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
|
||
|
// highlight-start
|
||
|
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;
|
||
|
}
|
||
|
// highlight-end
|
||
|
|
||
|
const make_width = ws => {
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="RNFA" label="RNFA">
|
||
|
|
||
|
Install `react-native-file-access` dependency:
|
||
|
|
||
|
```bash
|
||
|
npm i -S react-native-file-access@3.0.4
|
||
|
```
|
||
|
|
||
|
Add the highlighted lines to `index.js`:
|
||
|
|
||
|
```js title="index.js"
|
||
|
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
|
||
|
// highlight-start
|
||
|
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;
|
||
|
}
|
||
|
// highlight-end
|
||
|
|
||
|
const make_width = ws => {
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="RNFB" label="RNFB">
|
||
|
|
||
|
Install `rn-fetch-blob` dependency:
|
||
|
|
||
|
```bash
|
||
|
npm i -S rn-fetch-blob@0.12.0
|
||
|
```
|
||
|
|
||
|
Add the highlighted lines to `index.js`:
|
||
|
|
||
|
```js title="index.js"
|
||
|
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
|
||
|
// highlight-start
|
||
|
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;
|
||
|
}
|
||
|
// highlight-end
|
||
|
|
||
|
const make_width = ws => {
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="RNFS" label="RNFS">
|
||
|
|
||
|
Install `react-native-fs` dependency:
|
||
|
|
||
|
```bash
|
||
|
npm i -S react-native-fs@2.20.0
|
||
|
```
|
||
|
|
||
|
Add the highlighted lines to `index.js`:
|
||
|
|
||
|
```js title="index.js"
|
||
|
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
|
||
|
// highlight-start
|
||
|
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;
|
||
|
}
|
||
|
// highlight-end
|
||
|
|
||
|
const make_width = ws => {
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="EXPO" label="EXPO">
|
||
|
|
||
|
:::warning pass
|
||
|
|
||
|
At the time of testing, Expo Modules were incompatible with Android projects.
|
||
|
|
||
|
:::
|
||
|
|
||
|
Install `expo-file-system` and `expo-document-picker` dependencies:
|
||
|
|
||
|
```bash
|
||
|
npx install-expo-modules
|
||
|
npm i -S expo-file-system expo-document-picker
|
||
|
```
|
||
|
|
||
|
:::note pass
|
||
|
|
||
|
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`:
|
||
|
|
||
|
```js title="index.js"
|
||
|
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
|
||
|
// highlight-start
|
||
|
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;
|
||
|
}
|
||
|
// highlight-end
|
||
|
|
||
|
const make_width = ws => {
|
||
|
```
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
|
||
|
5) Refresh the app:
|
||
|
|
||
|
```bash
|
||
|
cd ios
|
||
|
pod install
|
||
|
cd ..
|
||
|
```
|
||
|
|
||
|
Once refreshed, the development process must be restarted:
|
||
|
|
||
|
```bash
|
||
|
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.
|
||
|
|
||
|
![save file iOS](pathname:///mobile/quasar7a.png)
|
||
|
|
||
|
- 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`:
|
||
|
|
||
|
![pick file iOS](pathname:///mobile/rnios2.png)
|
||
|
|
||
|
Once selected, the screen should refresh with new contents:
|
||
|
|
||
|
![read file iOS](pathname:///reactnative/ios3.png)
|
||
|
|
||
|
- Click "Export data". You will see a popup with a location:
|
||
|
|
||
|
![write file iOS](pathname:///reactnative/ios4.png)
|
||
|
|
||
|
- Find the file and verify the contents are correct:
|
||
|
|
||
|
```bash
|
||
|
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:
|
||
|
|
||
|
```bash
|
||
|
npx react-native run-android
|
||
|
```
|
||
|
|
||
|
![React Native Android App](pathname:///reactnative/and1.png)
|
||
|
|
||
|
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`:
|
||
|
|
||
|
![pick file Android](pathname:///mobile/rnand2.png)
|
||
|
|
||
|
Once selected, the screen should refresh with new contents:
|
||
|
|
||
|
![read file Android](pathname:///reactnative/and3.png)
|
||
|
|
||
|
- Click "Export data". You will see a popup with a location:
|
||
|
|
||
|
![write file Android](pathname:///reactnative/and4.png)
|
||
|
|
||
|
- Pull the file from the simulator and verify the contents:
|
||
|
|
||
|
```bash
|
||
|
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
|
||
|
npx xlsx-cli /tmp/sheetjsw.xlsx
|
||
|
```
|
||
|
|
||
|
[^1]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) and select the appropriate "Development OS".
|
||
|
[^2]: See ["Array of Arrays" in the API reference](/docs/api/utilities/array#array-of-arrays)
|
||
|
[^3]: See ["Array Output" in "Utility Functions"](/docs/api/utilities/array#array-output)
|
||
|
[^4]: See ["Array of Arrays Input" in "Utility Functions"](/docs/api/utilities/array#array-of-arrays-input)
|
||
|
[^5]: React-Native commit [`5b597b5`](https://github.com/facebook/react-native/commit/5b597b5ff94953accc635ed3090186baeecb3873) added the final piece required for `fetch` support. It landed in version `0.72.0-rc.1` and is available in official releases starting from `0.72.0`.
|
||
|
[^6]: When the demo was last tested, the Zulu11 distribution of Java 11 was installed through the macOS Brew package manager. [Direct downloads are available at `azul.com`](https://www.azul.com/downloads/?version=java-11-lts&package=jdk#zulu)
|
||
|
[^7]: See ["Running On Device"](https://reactnative.dev/docs/running-on-device) in the React Native documentation
|
||
|
[^8]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) for Android (and iOS, if applicable)
|