docs.sheetjs.com/docz/docs/03-demos/06-desktop/06-reactnative.md
2023-09-22 02:44:32 -04:00

24 KiB

title sidebar_label description pagination_prev pagination_next sidebar_position sidebar_custom_props
Sheets on the Desktop with React Native React Native Build data-intensive desktop apps with React Native. Seamlessly integrate spreadsheets into your app using SheetJS. Securely process and generate Excel files at the desk. demos/mobile/index demos/data/index 6
summary
Native Components with React

import current from '/version.js'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock';

React Native for Windows + macOS1 is a backend for React Native that supports native apps. The Windows backend builds apps for use on Windows 10 / 11, Xbox, and other supported platforms. The macOS backend supports macOS 10.14 SDK

SheetJS is a JavaScript library for reading and writing data from spreadsheets.

This demo uses React Native for Windows + macOS and SheetJS to process spreadsheets. We'll explore how to load SheetJS in a React Native deskktop app and create native modules for selecting and reading files from the computer.

The Windows and macOS demos create apps that look like the screenshots below:

Win10 macOS

Windows screenshot

macOS screenshot

:::note

This demo was tested in the following environments:

OS and Version Arch RN Platform Date
Windows 10 x64 v0.71.25 2023-07-24
Windows 11 x64 v0.71.11 2023-05-11
Windows 11 ARM v0.72.9 2023-09-18
MacOS 12.6 x64 v0.71.26 2023-07-23
MacOS 13.5.2 ARM v0.72.4 2023-09-18

:::

:::info pass

This section covers React Native for desktop applications. For iOS and Android applications, check the mobile demo

:::

:::warning Telemetry

React Native for Windows + macOS commands include telemetry without proper disclaimer or global opt-out.

The recommended approach for suppressing telemetry is explicitly passing the --no-telemetry flag. The following commands are known to support the flag:

  • Initializing a macOS project with react-native-macos-init
  • Initializing a Windows project with react-native-windows-init
  • Running Windows apps with react-native run-windows

:::

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.

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.

The state is initialized with the following snippet:

const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]);

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 */
  setAoA(data);
}

Displaying Data

The demos use native View elements from react-native to display data.

Explanation (click to show)

Since some spreadsheets may have empty cells between cells containing data, looping over the rows may skip values!

This example explicitly loops over the row and column indices.

Determining the Row Indices

The first row index is 0 and the last row index is aoa.length - 1. This corresponds to the for loop:

for(var R = 0; R < aoa.length; ++R) {/* ... */}

Determining the Column Indices

The first column index is 0 and the last column index must be calculated from the maximum column index across every row.

Traditionally this would be implemented in a for loop:

var max_col_index = 0;
for(var R = 0; R < aoa.length; ++R) {
  if(!aoa[R]) continue;
  max_col_index = Math.max(max_col_index, aoa[R].length - 1);
}

Array#reduce simplifies this calculation:

const max_col_index = aoa.reduce((C,row) => Math.max(C,row.length), 1) - 1;

Looping from 0 to N-1

Traditionally a for loop would be used:

var data = [];
for(var R = 0; R < max_row; ++R) data[R] = func(R);

For creating an array of React Native components, Array.from should be used:

var children = Array.from({length: max_row}, (_,R) => ( <Row key={R} /> ));

The relevant parts for rendering data are shown below:

import React, { useState, type FC } from 'react';
import { SafeAreaView, ScrollView, Text, View } from 'react-native';

const App: FC = () => {
  const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]);
  const max_cols = aoa.reduce((acc,row) => Math.max(acc,row.length),1);

  return (
    <SafeAreaView>
      <ScrollView contentInsetAdjustmentBehavior="automatic">
        {/* Table Container */}
        <View>{
          /* Loop over the row indices */
          // highlight-next-line
          Array.from({length: aoa.length}, (_, R) => (
            /* Table Row */
            <View key={R}>{
              /* Loop over the column indices */
              // highlight-next-line
              Array.from({length: max_cols}, (_, C) => (
                /* Table Cell */
                <View key={C}>
                   // highlight-next-line
                  <Text>{String(aoa?.[R]?.[C]??"")}</Text>
                </View>
              ))
            }</View>
          ))
        }</View>
      </ScrollView>
    </SafeAreaView>
  );
};
export default App;

Native Modules

:::caution

As with the mobile versions of React Native, file operations are not provided by the base SDK. The examples include native code for both Windows and macOS.

The Windows demo assumes some familiarity with C++ / C# and the macOS demo assumes some familiarity with Objective-C.

:::

React Native for Windows + macOS use Turbo Modules4 for native integrations.

The demos define a native module named DocumentPicker.

Reading Files

The native modules in the demos define a PickAndRead function that will show the file picker, read the file contents, and return a Base64 string.

Only the main UI thread can show file pickers. This is similar to Web Worker DOM access limitations in the Web platform.

Integration

This module can be referenced from the Turbo Module Registry:

import { read } from 'xlsx';
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
const DocumentPicker = getEnforcing('DocumentPicker');


/* ... in some event handler ... */
async() => {
  const b64 = await DocumentPicker.PickAndRead();
  const wb = read(b64);
  // DO SOMETHING WITH `wb` HERE
}

Native Module

React Native Windows supports C++ and C# projects.

[ReactMethod("PickAndRead")]
public async void PickAndRead(IReactPromise<string> result) {
  /* perform file picker action in the UI thread */
  // highlight-next-line
  context.Handle.UIDispatcher.Post(async() => { try {
    /* create file picker */
    var picker = new FileOpenPicker();
    picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
    picker.FileTypeFilter.Add(".xlsx");
    picker.FileTypeFilter.Add(".xls");

    /* show file picker */
    // highlight-next-line
    var file = await picker.PickSingleFileAsync();
    if(file == null) throw new Exception("File not found");

    /* read data and return base64 string */
    var buf = await FileIO.ReadBufferAsync(file);
    // highlight-next-line
    result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
  } catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
}
REACT_METHOD(PickAndRead);
void PickAndRead(ReactPromise<winrt::hstring> promise) noexcept {
  auto prom = promise;
  /* perform file picker action in the UI thread */
  // highlight-next-line
  context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget {
    auto p = prom; // promise -> prom -> p dance avoids promise destruction

    /* create file picker */
    FileOpenPicker picker;
    picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
    picker.FileTypeFilter().Append(L".xlsx");
    picker.FileTypeFilter().Append(L".xls");

    /* show file picker */
    // highlight-next-line
    StorageFile file = co_await picker.PickSingleFileAsync();
    if(file == nullptr) { p.Reject("File not Found"); co_return; }

    /* read data and return base64 string */
    auto buf = co_await FileIO::ReadBufferAsync(file);
    // highlight-next-line
    p.Resolve(CryptographicBuffer::EncodeToBase64String(buf));
    co_return;
  });
}

React Native macOS supports Objective-C modules

/* the resolve/reject is projected on the JS side as a Promise */
RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
  /* perform file picker action in the UI thread */
  // highlight-next-line
  RCTExecuteOnMainQueue(^{
    /* create file picker */
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseDirectories:NO];
    [panel setAllowsMultipleSelection:NO];
    [panel setMessage:@"Select a spreadsheet to read"];

    /* show file picker */
    // highlight-next-line
    [panel beginWithCompletionHandler:^(NSInteger result){
      if (result == NSModalResponseOK) {
        /* read data and return base64 string */
        NSURL *selected = [[panel URLs] objectAtIndex:0];
        NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil];
        if(hFile) {
          NSData *data = [hFile readDataToEndOfFile];
          // highlight-next-line
          resolve([data base64EncodedStringWithOptions:0]);
        } else reject(@"read_failure", @"Could not read selected file!", nil);
      } else reject(@"select_failure", @"No file selected!", nil);
    }];
  });
}

Windows Demo

:::warning pass

There is no simple standalone executable file at the end of the process.

The official documentation describes distribution strategies

:::

:::note

React Native Windows supports writing native code in C++ or C#. This demo has been tested against both application types.

:::

  1. Follow the "Getting Started" guide

:::caution

At the time of testing, NodeJS v16 was required. A tool like nvm-windows should be used to switch the NodeJS version.

:::

Project Setup

  1. Create a new project using React Native 0.72:
npx react-native init SheetJSWin --template react-native@^0.72.0
cd SheetJSWin
  1. Create the Windows part of the application:
npx react-native-windows-init --no-telemetry --overwrite --language=cs
npx react-native-windows-init --no-telemetry --overwrite
  1. Install the SheetJS library:

{\ npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz}

  1. To ensure that the app works, launch the app:
npx react-native run-windows --no-telemetry

:::caution

When the demo was tested in Windows 11, the run step failed with the message:

The Windows SDK version 10.0.19041.0 was not found

Specific Windows SDK versions can be installed through Visual Studio Installer.

:::

npx react-native run-windows --no-telemetry --arch=X86

:::warning pass

The ARM64 binary is normally built with

npx react-native run-windows --no-telemetry --arch=ARM64

When this demo was last tested on Windows 11 ARM, the build failed.

As it affects the starter project, it is a bug in ARM64 React Native Windows

:::

Native Module

  1. Download DocumentPicker.cs and save to windows\SheetJSWin\DocumentPicker.cs.
iwr -Uri https://docs.sheetjs.com/reactnative/DocumentPicker.cs -OutFile windows/SheetJSWin/DocumentPicker.cs
curl -Lo windows/SheetJSWin/DocumentPicker.cs https://docs.sheetjs.com/reactnative/DocumentPicker.cs
  1. Add the highlighted line to windows\SheetJSWin\SheetJSWin.csproj. Look for the ItemGroup that contains ReactPackageProvider.cs:
<!-- highlight-next-line -->
    <Compile Include="DocumentPicker.cs" />
    <Compile Include="ReactPackageProvider.cs" />
  </ItemGroup>
  1. Download DocumentPicker.h and save to windows\SheetJSWin\DocumentPicker.h.
iwr -Uri https://docs.sheetjs.com/reactnative/DocumentPicker.h -OutFile windows/SheetJSWin/DocumentPicker.h
curl -Lo windows/SheetJSWin/DocumentPicker.h https://docs.sheetjs.com/reactnative/DocumentPicker.h
  1. Add the highlighted line to windows\SheetJSWin\ReactPackageProvider.cpp:
#include "ReactPackageProvider.h"
// highlight-next-line
#include "DocumentPicker.h"
#include "NativeModules.h"

Now the native module will be added to the app.

Application

  1. Remove App.js (if it exists) and download App.tsx:
iwr -Uri https://docs.sheetjs.com/reactnative/rnw/App.tsx -OutFile App.tsx
curl -LO https://docs.sheetjs.com/reactnative/rnw/App.tsx
  1. Test the app again:
npx react-native run-windows --no-telemetry
npx react-native run-windows --no-telemetry --arch=X86

:::warning pass

The ARM64 binary is normally built with

npx react-native run-windows --no-telemetry --arch=ARM64

When this demo was last tested on Windows 11 ARM, the build failed.

As it affects the starter project, it is a bug in ARM64 React Native Windows

:::

Download https://sheetjs.com/pres.xlsx.

Click "Click here to Open File!" and use the file picker to select pres.xlsx . The app will refresh and display the data from the file.

macOS Demo

  1. Follow the "Setting up the development environment"5 guide in the React Native documentation for "React Native CLI Quickstart" + "macOS" + "iOS".

Project Setup

  1. Create a new React Native project using React Native 0.72:
npx -y react-native init SheetJSmacOS --template react-native@^0.72.0
cd SheetJSmacOS
  1. Create the MacOS part of the application:
npx -y react-native-macos-init --no-telemetry
  1. Install the SheetJS library:

{\ npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz}

  1. To ensure that the app works, launch the app:
npx react-native run-macos

Close the running app from the dock and close the Metro terminal window.

Native Module

  1. Create the file macos/SheetJSmacOS-macOS/RCTDocumentPicker.h with the following contents:
#import <React/RCTBridgeModule.h>
@interface RCTDocumentPicker : NSObject <RCTBridgeModule>
@end
  1. Create the file macos/SheetJSmacOS-macOS/RCTDocumentPicker.m with the following contents:
#import <Foundation/Foundation.h>
#import <React/RCTUtils.h>

#import "RCTDocumentPicker.h"

@implementation RCTDocumentPicker

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
  RCTExecuteOnMainQueue(^{
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseDirectories:NO];
    [panel setAllowsMultipleSelection:NO];
    [panel setMessage:@"Select a spreadsheet to read"];

    [panel beginWithCompletionHandler:^(NSInteger result){
      if (result == NSModalResponseOK) {
        NSURL *selected = [[panel URLs] objectAtIndex:0];
        NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil];
        if(hFile) {
          NSData *data = [hFile readDataToEndOfFile];
          resolve([data base64EncodedStringWithOptions:0]);
        } else reject(@"read_failure", @"Could not read selected file!", nil);
      } else reject(@"select_failure", @"No file selected!", nil);
    }];
  });
}
@end
  1. Edit the project file macos/SheetJSmacOS.xcodeproj/project.pbxproj.

There are four places where lines must be added:

:::note pass

A) Copy the highlighted line and paste under /* Begin PBXBuildFile section */:

/* Begin PBXBuildFile section */
// highlight-next-line
    4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */; };
    5142014D2437B4B30078DB4F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5142014C2437B4B30078DB4F /* AppDelegate.mm */; };

:::

:::note pass

B) Copy the highlighted lines and paste under /* Begin PBXFileReference section */:

/* Begin PBXFileReference section */
// highlight-start
    4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTDocumentPicker.h; path = "SheetJSMacOS-macOS/RCTDocumentPicker.h"; sourceTree = "<group>"; };
    4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTDocumentPicker.m; path = "SheetJSMacOS-macOS/RCTDocumentPicker.m"; sourceTree = "<group>"; };
// highlight-end
    008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };

:::

:::note pass

C) The goal is to add a reference to the PBXSourcesBuildPhase block for the macOS target. To determine this, look in the PBXNativeTarget section for a block with the comment SheetJSmacOS-macOS:

/* Begin PBXNativeTarget section */
...
      productType = "com.apple.product-type.application";
    };
// highlight-next-line
    514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = {
      isa = PBXNativeTarget;
...
/* End PBXNativeTarget section */

Within the block, look for buildPhases and find the hex string for Sources:

    514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = {
      isa = PBXNativeTarget;
      buildConfigurationList = 5142015A2437B4B40078DB4F /* Build configuration list for PBXNativeTarget "SheetJSmacOS-macOS" */;
      buildPhases = (
        1A938104A937498D81B3BD3B /* [CP] Check Pods Manifest.lock */,
        381D8A6F24576A6C00465D17 /* Start Packager */,
// highlight-next-line
        514201452437B4B30078DB4F /* Sources */,
        514201462437B4B30078DB4F /* Frameworks */,
        514201472437B4B30078DB4F /* Resources */,
        381D8A6E24576A4E00465D17 /* Bundle React Native code and images */,
        3689826CA944E2EF44FCBC17 /* [CP] Copy Pods Resources */,
      );

Search for that hex string (514201452437B4B30078DB4F in our example) in the file and it should show up in a PBXSourcesBuildPhase section. Within files, add the highlighted line:

    514201452437B4B30078DB4F /* Sources */ = {
      isa = PBXSourcesBuildPhase;
      buildActionMask = 2147483647;
      files = (
// highlight-next-line
        4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */,
        514201582437B4B40078DB4F /* main.m in Sources */,
        5142014D2437B4B30078DB4F /* AppDelegate.mm in Sources */,
      );
      runOnlyForDeploymentPostprocessing = 0;
    };

:::

:::note pass

D) The goal is to add file references to the "main group". Search for /* Begin PBXProject section */ and there should be one Project object. Within the project object, look for mainGroup:

/* Begin PBXProject section */
    83CBB9F71A601CBA00E9B192 /* Project object */ = {
      isa = PBXProject;
...
        Base,
      );
// highlight-next-line
      mainGroup = 83CBB9F61A601CBA00E9B192;
      productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
...
/* End PBXProject section */

Search for that hex string (83CBB9F61A601CBA00E9B192 in our example) in the file and it should show up in a PBXGroup section. Within children, add the highlighted lines:

    83CBB9F61A601CBA00E9B192 = {
      isa = PBXGroup;
      children = (
// highlight-start
        4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */,
        4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */,
// highlight-end
        5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */,
        13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */,

:::

  1. To ensure that the app still works, launch the app again:
npx react-native run-macos

Close the running app from the dock and close the Metro terminal window.

Application

  1. Download App.tsx and replace the file in the project:
curl -LO https://docs.sheetjs.com/reactnative/rnm/App.tsx
  1. Test the app:
npx react-native run-macos

Download https://sheetjs.com/pres.xlsx.

Click "Click here to Open File!" and use the file picker to select pres.xlsx . The app will refresh and display the data from the file.

  1. Make a release build:
xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release

The last line of the output will include the path to the app. If it is not displayed, the app path can be found in the DerivedData folder:

find ~/Library/Developer/Xcode/DerivedData -name SheetJSmacOS.app | grep Release
  1. Run the release app:
open -a "$(find ~/Library/Developer/Xcode/DerivedData -name SheetJSmacOS.app | grep Release | head -n 1)"

  1. The official website covers both platforms, but there are separate repositories for Windows and macOS ↩︎

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

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

  4. See "Turbo Native Modules" in the React Native documentation. ↩︎

  5. See "Setting up the development environment" in the React Native documentation. Select the "React Native CLI Quickstart" tab and choose the Development OS "macOS" and the Target OS "iOS". ↩︎