docs.sheetjs.com/docz/docs/03-demos/03-desktop/06-reactnative.md

684 lines
21 KiB
Markdown
Raw Normal View History

2023-01-05 03:57:48 +00:00
---
title: React Native for Desktop
pagination_prev: demos/mobile
pagination_next: demos/grid
sidebar_position: 6
sidebar_custom_props:
summary: Native Components with React
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::note
This section covers React Native for desktop applications. For iOS and Android
applications, [check the mobile demo](/docs/demos/mobile)
:::
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](/docs/getting-started/installation/nodejs) 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:
<table><thead><tr>
<th><a href="#windows-demo">Windows</a></th>
<th><a href="#macos-demo">macOS</a></th>
</tr></thead><tbody><tr><td>
![Windows screenshot](pathname:///reactnative/rnw.png)
</td><td>
![macOS screenshot](pathname:///reactnative/rnm.png)
</td></tr></tbody></table>
## 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](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules)
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:
```js
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_
<Tabs groupId="os">
<TabItem value="win" label="Windows">
React Native Windows supports C++ and C# projects.
<Tabs groupId="rnwlang">
<TabItem value="cs" label="C#">
```csharp
[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 }); }});
}
```
</TabItem>
<TabItem value="cpp" label="C++">
```cpp
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;
});
}
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="mac" label="macOS">
React Native macOS supports Objective-C modules
```objc
/* 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);
}];
});
}
```
</TabItem>
</Tabs>
## Windows Demo
This demo was tested against `v0.70.10` on 2023 January 04 in Windows 10.
:::warning
There is no simple standalone executable file at the end of the process.
[The official documentation describes distribution strategies](https://microsoft.github.io/react-native-windows/docs/native-code#distribution)
:::
:::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](https://microsoft.github.io/react-native-windows/docs/getting-started)
:::caution
NodeJS `v16` is required. There are OS-specific tools for downgrading:
- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows
- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc.
:::
1) Create a new project using React Native `0.70`:
```powershell
npx react-native init SheetJSWin --template react-native@^0.70.0
cd .\SheetJSWin\
```
Create the Windows part of the application:
<Tabs groupId="rnwlang">
<TabItem value="cs" label="C#">
```powershell
npx react-native-windows-init --no-telemetry --overwrite --language=cs
```
</TabItem>
<TabItem value="cpp" label="C++">
```powershell
npx react-native-windows-init --no-telemetry --overwrite
```
</TabItem>
</Tabs>
Install library:
```powershell
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
```
To ensure that the app works, launch the app:
```powershell
npx react-native run-windows --no-telemetry
```
<Tabs groupId="rnwlang">
<TabItem value="cs" label="C#">
2) Create the file `windows\SheetJSWin\DocumentPicker.cs` with the following:
```csharp title="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`:
```xml title="windows\SheetJSWin\SheetJSWin.csproj"
<!-- highlight-next-line -->
<Compile Include="DocumentPicker.cs" />
<Compile Include="ReactPackageProvider.cs" />
</ItemGroup>
```
</TabItem>
<TabItem value="cpp" label="C++">
2) Create the file `windows\SheetJSWin\DocumentPicker.h` with the following:
```cpp title="windows\SheetJSWin\DocumentPicker.h"
#pragma once
#include <winrt/Windows.Storage.Pickers.h>
#include <winrt/Windows.Security.Cryptography.h>
#include "NativeModules.h"
using namespace winrt::Microsoft::ReactNative;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Pickers;
using namespace winrt::Windows::Security::Cryptography;
namespace SheetJSWin {
REACT_MODULE(DocumentPicker);
struct DocumentPicker {
REACT_INIT(Initialize);
void Initialize(const ReactContext& reactContext) noexcept {
context = reactContext;
}
REACT_METHOD(PickAndRead);
void PickAndRead(ReactPromise<winrt::hstring> promise) noexcept {
auto prom = promise;
context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget {
auto p = prom;
FileOpenPicker picker;
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
picker.FileTypeFilter().Append(L".xlsx");
picker.FileTypeFilter().Append(L".xls");
StorageFile file = co_await picker.PickSingleFileAsync();
if(file == nullptr) { p.Reject("File not Found"); co_return; }
auto buf = co_await FileIO::ReadBufferAsync(file);
p.Resolve(CryptographicBuffer::EncodeToBase64String(buf));
co_return;
});
}
private:
ReactContext context{nullptr};
};
}
```
3) Add the highlighted line to `windows\SheetJSWin\ReactPackageProvider.cpp`:
```cpp title="windows\SheetJSWin\ReactPackageProvider.cpp"
#include "ReactPackageProvider.h"
// highlight-next-line
#include "DocumentPicker.h"
#include "NativeModules.h"
```
</TabItem>
</Tabs>
Now the native module will be added to the app.
4) Remove `App.js` and save the following to `App.tsx`:
```tsx title="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:
```powershell
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](https://reactnative.dev/docs/environment-setup)
guide for React Native CLI on MacOS.
:::caution
NodeJS `v16` is required. There are OS-specific tools for downgrading:
- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows
- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc.
:::
1) Create a new project using React Native `0.64`:
```bash
npx react-native init SheetJSmacOS --template react-native@^0.64.0
cd SheetJSmacOS
```
Create the MacOS part of the application:
```bash
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:
```powershell
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`:
```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.h"
#import <React/RCTBridgeModule.h>
@interface RCTDocumentPicker : NSObject <RCTBridgeModule>
@end
```
Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.m`:
```objc title="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 */`
```plist
/* Begin PBXBuildFile section */
// highlight-next-line
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 */`
```plist
/* 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>"; };
```
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`:
```plist
/* 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`:
```plist
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:
```plist
514201452437B4B30078DB4F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
// highlight-next-line
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`:
```plist
/* 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:
```plist
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
// highlight-start
4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */,
4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */,
// highlight-end
5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */,
13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */,
```
4) Replace `App.js` with the following:
```tsx title="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:
```bash
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:
```bash
xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release
```