docs.sheetjs.com/docz/docs/03-demos/05-mobile/01-reactnative.md
2023-08-20 16:39:35 -04:00

25 KiB

title sidebar_label description pagination_prev pagination_next sidebar_position sidebar_custom_props
React Native React Native 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. demos/static/index demos/desktop/index 1
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 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:

The "Complete Example" creates an app that looks like the screenshots below:

iOS Android

iOS screenshot

Android screenshot

:::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 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.

SpreadsheetArray of Arrays

pres.xlsx data

[
  ["Name", "Index"],
  ["Bill Clinton", 42],
  ["GeorgeW Bush", 43],
  ["Barack Obama", 44],
  ["Donald Trump", 45],
  ["Joseph Biden", 46]
]

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_json3 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_sheet4 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.05 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

:::note

This demo was tested on an Intel Mac on 2023 August 20 with RN 0.72.4.

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:
npx -y react-native@0.72.4 init SheetJSRNFetch --version="0.72.4"
  1. Install shared dependencies:

{\ 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}

  1. Download App.tsx and replace:
curl -LO https://docs.sheetjs.com/reactnative/App.tsx

Android Testing

  1. Install or switch to Java 116

:::note pass

When the demo was last tested on macOS, java -version displayed the following:

openjdk version "11.0.20" 2023-07-18 LTS
OpenJDK Runtime Environment Zulu11.66+15-CA (build 11.0.20+8-LTS)
OpenJDK 64-Bit Server VM Zulu11.66+15-CA (build 11.0.20+8-LTS, mixed mode)

:::

  1. Start the Android emulator:
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.

:::

  1. 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

before screenshot

after screenshot

iOS Testing

:::warning pass

iOS testing requires macOS. It does not work on Windows or Linux.

:::

  1. Refresh iOS project by running pod install from the ios subfolder:
cd ios; pod install; cd ..
  1. Start the iOS emulator:
npx react-native run-ios
  1. 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

before screenshot

after screenshot

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

Selecting a file (click to show)

The setting copyTo: "cachesDirectory" must be set:

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

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({
  // 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

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.

Reading and Writing snippets (click to show)

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'});

:::caution pass

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({
// 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

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 show)

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 show)

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

:::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 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

:::note

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.

The Android simulator runs Android 12 (S) Platform 31 on a Pixel 5.

:::

:::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.7 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.1"
  1. Install shared dependencies:

{\ 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}

Refresh iOS project by running pod install from the ios subfolder:

cd ios
pod install
cd ..
  1. 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:

React Native iOS App

  1. Pick a filesystem library for integration:

Install react-native-blob-util dependency:

npm i -S react-native-blob-util@0.17.1

Add the highlighted lines to 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 => {

Install react-native-file-access dependency:

npm i -S react-native-file-access@2.6.0

Add the highlighted lines to 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 => {

Install rn-fetch-blob dependency:

npm i -S rn-fetch-blob@0.12.0

Add the highlighted lines to 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 => {

Install react-native-fs dependency:

npm i -S react-native-fs@2.20.0

Add the highlighted lines to 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 => {

:::warning pass

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

Add the highlighted lines to 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 => {
  1. 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.

save file iOS

  • 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

Once selected, the screen should refresh with new contents:

read file iOS

  • Click "Export data". You will see a popup with a location:

write file iOS

  • 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

React Native Android App

The app can be tested with the following sequence in the simulator:

pick file Android

Once selected, the screen should refresh with new contents:

read file Android

  • Click "Export data". You will see a popup with a location:

write file Android

  • 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

  1. Follow the "React Native CLI Quickstart" and select the appropriate "Development OS". ↩︎

  2. See "Array of Arrays" in the API reference ↩︎

  3. See "Array Output" in "Utility Functions" ↩︎

  4. See "Array of Arrays Input" in "Utility Functions" ↩︎

  5. React-Native commit 5b597b5 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 ↩︎

  7. Follow the "React Native CLI Quickstart" for Android (and iOS, if applicable) ↩︎