Skip to main content

React Native for Desktop

note

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

React Native for Windows + macOS 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

The NodeJS Module can be imported from the main app script. File operations must be written in native code.

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

WindowsmacOS

Windows screenshot

macOS screenshot

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 Modules for effortless integration with native libraries and code.

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 */
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 */
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);
result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
} catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
}

Windows Demo

This demo was tested against v0.70.10 on 2023 January 04 in Windows 10.

danger

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.

0) 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.

1) Create a new project using React Native 0.70:

npx react-native init SheetJSWin --template [email protected]^0.70.0
cd .\SheetJSWin\

Create the Windows part of the application:

npx react-native-windows-init --no-telemetry --overwrite --language=cs

Install library:

npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz

To ensure that the app works, launch the app:

npx react-native run-windows --no-telemetry

2) Create the file windows\SheetJSWin\DocumentPicker.cs with the following:

windows\SheetJSWin\DocumentPicker.cs
using System;
using System.Threading.Tasks;
using Windows.Security.Cryptography;
using Windows.Storage;
using Windows.Storage.Pickers;
using Microsoft.ReactNative.Managed;

namespace SheetJSWin {
[ReactModule]
class DocumentPicker {
private ReactContext context;
[ReactInitializer]
public void Initialize(ReactContext reactContext) { context = reactContext; }

[ReactMethod("PickAndRead")]
public async void PickAndRead(IReactPromise<string> result) {
context.Handle.UIDispatcher.Post(async() => { try {
var picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
picker.FileTypeFilter.Add(".xlsx");
picker.FileTypeFilter.Add(".xls");

var file = await picker.PickSingleFileAsync();
if(file == null) throw new Exception("File not found");

var buf = await FileIO.ReadBufferAsync(file);
result.Resolve(CryptographicBuffer.EncodeToBase64String(buf));
} catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }});
}
}
}

3) Add the highlighted line to windows\SheetJSWin\SheetJSWin.csproj. Look for the ItemGroup that contains ReactPackageProvider.cs:

windows\SheetJSWin\SheetJSWin.csproj
    <Compile Include="DocumentPicker.cs" />
<Compile Include="ReactPackageProvider.cs" />
</ItemGroup>

Now the native module will be added to the app.

4) Remove App.js and save the following to App.tsx:

App.tsx
import React, { useState, type Node } from 'react';
import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
import { read, utils, version } from 'xlsx';
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
const DocumentPicker = getEnforcing('DocumentPicker');

const App: () => Node = () => {

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

return (
<SafeAreaView style={styles.outer}>
<Text style={styles.title}>SheetJS × React Native Windows {version}</Text>
<TouchableHighlight onPress={async() => {
try {
const b64 = await DocumentPicker.PickAndRead();
const wb = read(b64);
setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } ));
} catch(err) { alert(`Error: ${err.message}`); }
}}><Text style={styles.button}>Click here to Open File!</Text></TouchableHighlight>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={styles.table}>{aoa.map((row,R) => (
<View style={styles.row} key={R}>{row.map((cell,C) => (
<View style={styles.cell} key={C}><Text>{cell}</Text></View>
))}</View>
))}</View>
</ScrollView>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
cell: { flex: 4 },
row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', },
table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', },
outer: { marginTop: 32, paddingHorizontal: 24, },
title: { fontSize: 24, fontWeight: '600', },
button: { marginTop: 8, fontSize: 18, fontWeight: '400', },
});

export default App;

5) Test the app again:

npx react-native run-windows --no-telemetry

Download https://sheetjs.com/pres.xlsx, then click on "open file". Use the file picker to select the pres.xlsx file and the app will show the data.

macOS Demo

This demo was tested against v0.64.30 on 2023 January 04 in MacOS 12.4

0) Follow the React Native guide for React Native CLI on MacOS.

caution

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

1) Create a new project using React Native 0.64:

npx react-native init SheetJSmacOS --template [email protected]^0.64.0
cd SheetJSmacOS

Create the MacOS part of the application:

npx react-native-macos-init --no-telemetry

Install Library:

npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz

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.

2) Create the file macos/SheetJSmacOS-macOS/RCTDocumentPicker.h:

macos/SheetJSmacOS-macOS/RCTDocumentPicker.h
#import <React/RCTBridgeModule.h>
@interface RCTDocumentPicker : NSObject <RCTBridgeModule>
@end

Create the file macos/SheetJSmacOS-macOS/RCTDocumentPicker.m:

macos/SheetJSmacOS-macOS/RCTDocumentPicker.m
#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

3) Edit the project file macos/SheetJSmacOS.xcodeproj/project.pbxproj.

There are four places where lines must be added:

A) Immediately after /* Begin PBXBuildFile section */

/* Begin PBXBuildFile section */
4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */; };
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };

B) Immediately after /* Begin PBXFileReference section */

/* Begin PBXFileReference section */
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>"; };
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };

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";
};
514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = {
isa = PBXNativeTarget;
...
/* End PBXNativeTarget section */

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

      buildPhases = (
1A938104A937498D81B3BD3B /* [CP] Check Pods Manifest.lock */,
381D8A6F24576A6C00465D17 /* Start Packager */,
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 = (
4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */,
514201502437B4B30078DB4F /* ViewController.m in Sources */,
514201582437B4B40078DB4F /* main.m in Sources */,
5142014D2437B4B30078DB4F /* AppDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

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,
);
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 = (
4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */,
4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */,
5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */,
13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */,

4) Replace App.js with the following:

App.js
import React, { useState, type Node } from 'react';
import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
import { read, utils, version } from 'xlsx';
import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
const DocumentPicker = getEnforcing('DocumentPicker');

const App: () => Node = () => {

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

return (
<SafeAreaView style={styles.outer}>
<Text style={styles.title}>SheetJS × React Native MacOS {version}</Text>
<TouchableHighlight onPress={async() => {
try {
const b64 = await DocumentPicker.PickAndRead();
const wb = read(b64);
setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } ));
} catch(err) { alert(`Error: ${err.message}`); }
}}><Text style={styles.button}>Click here to Open File!</Text></TouchableHighlight>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={styles.table}>{aoa.map((row,R) => (
<View style={styles.row} key={R}>{row.map((cell,C) => (
<View style={styles.cell} key={C}><Text>{cell}</Text></View>
))}</View>
))}</View>
</ScrollView>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
cell: { flex: 4 },
row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', },
table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', },
outer: { marginTop: 32, paddingHorizontal: 24, },
title: { fontSize: 24, fontWeight: '600', },
button: { marginTop: 8, fontSize: 18, fontWeight: '400', },
});

export default App;

5) Test the app:

npx react-native run-macos

Download https://sheetjs.com/pres.xlsx, then click on "open file". Use the file picker to select the pres.xlsx file and the app will show the data.

6) Make a release build:

xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release