This commit is contained in:
SheetJS 2023-07-02 02:45:10 -04:00
parent 5f52a9f6b6
commit 74ac713193

@ -1,5 +1,7 @@
---
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
@ -7,13 +9,26 @@ 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';
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from the main `App.js` entrypoint or any script in the project.
[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 "Complete Example" creates an app that looks like the screenshots below:
@ -30,17 +45,9 @@ The "Complete Example" creates an app that looks like the screenshots below:
</td></tr></tbody></table>
["Fetching Remote Data"](#fetching-remote-data) uses the built-in `fetch` to
download and parse remote workbook files.
:::caution pass
["Native Libraries"](#native-libraries) uses native libraries to read and write
files in the local device.
:::caution
**Before reading this demo, follow the official React Native CLI Guide!**
Development Environment Guide: <https://reactnative.dev/docs/environment-setup>
**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
@ -48,42 +55,178 @@ 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 the main `App.js` entrypoint or any script in the project.
### 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
:::info
React Native versions starting from `0.72.0`[^5] support binary data with `fetch`.
React Native `0.72.0` will support binary data with `fetch`. For older versions,
[a native library](#native-libraries) can provide support.
:::
React Native 0.72.0 will 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.numbers")).arrayBuffer();
const ab = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
/* parse data */
const wb = XLSX.read(ab);
```
### Fetch Demo
The following demo uses `react-native-table-component` to display the first
worksheet in a simple table.
:::note
This demo was tested on an Intel Mac on 2023 April 24 with RN `0.72.0-rc.1`.
This demo was tested on an Intel Mac on 2023 July 02 with RN `0.72.1`.
The iOS simulator runs iOS 16.2 on an iPhone SE (3rd generation).
The Android simulator runs Android 12.0 (S) API 31 on a Pixel 3.
:::
1) Create project:
```bash
npx react-native init SheetJSRNFetch --version="0.72.0-rc.1"
npx -y react-native@0.72.1 init SheetJSRNFetch --version="0.72.1"
```
2) Install shared dependencies:
@ -95,69 +238,46 @@ 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>
Refresh iOS project by running `pod install` from the `ios` subfolder:
```bash
cd ios; pod install; cd ..
```
3) Download [`App.tsx`](pathname:///reactnative/App.tsx) and replace:
```bash
curl -LO https://docs.sheetjs.com/reactnative/App.tsx
```
**iOS Testing**
Start the iOS emulator:
```bash
npx react-native run-ios
```
When opened, the app should look like the "Before" screenshot below. After
tapping "Import data from a spreadsheet", the app should show 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 Testing**
Start the Android emulator:
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.19" 2023-04-18 LTS
OpenJDK Runtime Environment Zulu11.64+19-CA (build 11.0.19+7-LTS)
OpenJDK 64-Bit Server VM Zulu11.64+19-CA (build 11.0.19+7-LTS, mixed mode)
```
:::
5) Start the Android emulator:
```bash
npx react-native run-android
```
:::note
:::caution pass
When this demo was last tested, the simulator failed with the message
If the initial launch fails with an error referencing the emulator, manually
start the emulator and try again.
> Unable to load script. Make sure you're either Running Metro ...
The workaround is to launch Metro directly:
```bash
npx react-native start
```
Press `a` in the terminal window and Metro will try to reload the app.
Gradle errors typically stem from a Java version mismatch. Run `java -version`
and verify that the Java major version is 11.
:::
When opened, the app should look like the "Before" screenshot below. After
tapping "Import data from a spreadsheet", the app should show new data:
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>
@ -172,10 +292,45 @@ tapping "Import data from a spreadsheet", the app should show new data:
</td></tr></tbody></table>
**iOS Testing**
## Native Libraries
:::warning pass
:::warning
iOS testing requires macOS. It does not work on Windows.
:::
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>
## 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.
@ -187,40 +342,41 @@ consulted before picking a library.
:::
The following table lists tested file plugins. "OS" lists tested platforms
("A" for Android and "I" for iOS). "Copy" indicates whether an explicit copy
is needed (file picker copies to cache directory and file plugin reads cache).
("A" for Android and "I" for iOS).
| File system Plugin | File Picker Plugin | OS | Copy |
|:---------------------------|:-------------------------------|:----:|:-----|
| `react-native-file-access` | `react-native-document-picker` | `AI` | |
| `react-native-blob-util` | `react-native-document-picker` | `AI` | YES |
| `rn-fetch-blob` | `react-native-document-picker` | `AI` | YES |
| `react-native-fs` | `react-native-document-picker` | `AI` | YES |
| `expo-file-system` | `expo-document-picker` | ` I` | YES |
| 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 show)</summary>
When a copy is not needed:
The setting `copyTo: "cachesDirectory"` must be set:
```js
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({allowMultiSelection: false, mode: "open" });
const path = f.uri; // this path can be read by RN file plugins
```
When a copy is needed:
```js
import { pickSingle } from 'react-native-document-picker';
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
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
```
@ -240,7 +396,7 @@ const result = await DocumentPicker.getDocumentAsync({
copyToCacheDirectory: true,
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
});
const path = result.uri;
const path = result.uri; // this path can be read by RN file plugins
```
</details>
@ -433,11 +589,11 @@ await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx
</details>
## Demo
### Demo
:::note
This demo was tested on an Intel Mac on 2023 April 30 with RN `0.72.0-rc.1`.
This demo was tested on an Intel Mac on 2023 July 02 with RN `0.72.1`.
The iOS simulator runs iOS 16.2 on an iPhone 14.
@ -449,8 +605,8 @@ The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5.
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. Details like creating an Android Virtual Device
are not covered here.
before approaching this demo.[^7] Details including Android Virtual Device
configuration are not covered here.
:::
@ -459,7 +615,7 @@ This example tries to separate the library-specific functions.
1) Create project:
```bash
npx react-native init SheetJSRN --version="0.72.0-rc.1"
npx react-native init SheetJSRN --version="0.72.1"
```
2) Install shared dependencies:
@ -519,7 +675,6 @@ import { Platform } from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
async function pickAndParse() {
/* rn-fetch-blob / react-native-blob-util need a copy */
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 + "/");
@ -560,11 +715,6 @@ import { Platform } from 'react-native';
import { Dirs, FileSystem } from 'react-native-file-access';
async function pickAndParse() {
/* react-native-file-access in RN < 0.72 does not need a copy */
//const f = await pickSingle({allowMultiSelection: false, mode: "open" });
//const res = await FileSystem.readFile(f.uri, "base64");
//return read(res, {type: 'base64'});
/* react-native-file-access in RN >= 0.72 needs a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
const res = await (await fetch(path)).arrayBuffer();
@ -603,7 +753,6 @@ import { Platform } from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
async function pickAndParse() {
/* rn-fetch-blob / react-native-blob-util need a copy */
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 + "/");
@ -643,7 +792,6 @@ import { pickSingle } from 'react-native-document-picker';
import { writeFile, readFile, DocumentDirectoryPath } from 'react-native-fs';
async function pickAndParse() {
/* react-native-fs needs a copy */
const f = await pickSingle({allowMultiSelection: false, copyTo: "cachesDirectory", mode: "open" });
const bstr = await readFile(f.fileCopyUri, 'ascii');
return read(bstr, {type:'binary'});
@ -665,12 +813,7 @@ const make_width = ws => {
:::warning
At the time of testing, Expo did not support RN 0.72 . The project should be
created with React Native 0.67.2:
```bash
npx react-native init SheetJSRN --version="0.67.2"
```
At the time of testing, Expo Modules were incompatible with Android projects.
:::
@ -793,3 +936,11 @@ Once selected, the screen should refresh with new contents:
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]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) for Android (and iOS, if applicable)