Exploring React Native on Windows

This is the second in our series of blog posts around Windows desktop technologies. After exploring Electron, we decided to spend time investigating a brand new technology: React Native for Windows.

One of our goals was to assess how much mutualization can happen between a Windows React Native code and a Web ReactJS code.

We want to share our experience and our evaluation of the technology, considering that this was done a few months ago and that technology evolves a lot in time.

What is React Native

React Native is an open-source mobile application framework created by Facebook. It is used to develop applications mainly for Android and iOS by enabling developers to use native platform capabilities along with ReactJS, a popular web UI framework. Two years ago, Microsoft released a first alpha version of Facebook’s React Native that added support for Windows 10 SDK, which allows building UWP apps. Microsoft Skype is one of the largest React Native applications in the world.

Exploring React Native for Windows

React Native uses Javascript (or TypeScript) to describe the UI, which is translated into a native component. On Windows, React Native components become WPF or UWP components at runtime.

When exploring this technology, we wanted to make sure that we were able to:

  • Provide custom components
  • Have asynchronous communication between frontend and backend
  • Reuse our web code base as much as possible

Custom components

Additional functionalities can be easily provided using custom modules. A module is a C# class that extends the ReactContextNativeModuleBase class and implements some methods callable by Javascript. A custom module can also provide additional UI components.

Adding native methods callable from Javascript
Let’s see how to implement a simple custom module that provides methods callable from Javascript.

  1. Create a CustomNativeModule class, extending ReactContextNativeModuleBase

// CustomModule.cs
  
using Newtonsoft.Json.Linq;
using ReactNative.Bridge;
using ReactNative.Collections;
using System;
using System.Collections.Generic;
  
namespace ReactNative.Modules.Costum
{
    public class CustomModule : ReactContextNativeModuleBase
    {
        public CustomModule(ReactContext reactContext)
            : base(reactContext)
        {
        }
    }
}

2. Implement the Name property, required by the abstract class ReactContextNativeModuleBase

private const string NAME = "CustomModule";
...
public override string Name
{
    get
    {
        return NAME;
    }
}

3. To expose a method to JavaScript a .NET method must be annotated using the [ReactMethod] attribute. The return type of bridge methods is always void. The React Native bridge is asynchronous, so the only way to pass a result to JavaScript is by using callbacks or emitting events

[ReactMethod]
public void CustomMethod (JObject object)
{
    Debug.WriteLine(NAME + ": CustomMethod called");
}
  
  
[ReactMethod]
public void CustomMethodWithArgs (JObject object)
{
    var name = object.Value<string>("name");
    Debug.WriteLine(NAME + ": CustomMethodWithArgs called with parameter name " + name);
}

4. Register the module. Create a CustomPackage implementing the IReactPackage interface, and add it to the Packages property of your main.cs

// CustomPackage.cs
 
public class CustomPackage : IReactPackage
{
    public IReadOnlyList<INativeModule> CreateNativeModules(ReactContext reactContext)
    {
        return new List<INativeModule>
        {
            new CustomModule(reactContext)
        };
    }
 
    public IReadOnlyList<Type> CreateJavaScriptModulesConfig()
    {
        return new List<Type>(0);
    }
 
    public IReadOnlyList<IViewManager> CreateViewManagers(
        ReactContext reactContext)
    {
        return new List<IViewManager>(0);
    }
}
// main.cs
...
public override List<IReactPackage> Packages
{
    get
    {
        return new List<IReactPackage>
        {
            new MainReactPackage(),
            new CustomPackage()
        };
    }
}

5. To make it simpler to access to the new functionality from JavaScript, wrap the native module in a JavaScript module

// custom_module.js
 
'use strict';
 
import { NativeModules } from 'react-native';
module.exports = NativeModules.CustomModule;

6. Now the native module’s methods are callable from Javascript

//app.js
 
import CustomModule from './custom_module'
 
CustomModule.CustomMethod();
CustomModule.CustomMethodWithArgs({
    name: 'Some JS name'
});

Adding a custom view
React Native enables wrapping up purely native view components for seamless integration with the application. In this guide we will implement a QrCodeView.

1. Create a ReactQrCodeViewManager class, extending SimpleViewManager with generic argument Border. Border is the type of the framework element managed by the manager: this will be the custom native view. The Name property is used to reference the native view type from JavaScript. The Border type is used to support border radii on images: the background of the Border is set to an ImageSource that represents the QrCode.

class ReactQrCodeViewManager : SimpleViewManager<Border>
{
    protected string _data;
 
    public override string Name
    {
        get
        {
            return "WindowsQrCodeView";
        }
    }

2. Implement overridden method CreateViewIntance. Views are created in the CreateViewInstance method, the view should initialize itself in its default state, any properties will be set via a follow up call to UpdateView.

protected override Border CreateViewInstance(ThemedReactContext reactContext)
{
    return new Border {
        Background = new ImageBrush()
    };
}

3. Expose view property setters using [ReactProp] attribute. Here we expose data, used to generate the qr code.

[ReactProp("data")]
public void SetSource(Border view, string data)
{
    _data = data;
    UpdateQrCode(view);
}

async protected void UpdateQrCode(Border view)
{
    var qrGenerator = new QRCodeGenerator();
    var qrCodeData = qrGenerator.CreateQrCode(_data, _eccLevel);
    var qrCode = new BitmapByteQRCode(qrCodeData);
    var pixelPerModule = 20;
    var qrCodeImage = qrCode.GetGraphic(pixelPerModule);
 
    var bitmap = new BitmapImage();
    var imageBrush = (ImageBrush)view.Background;
    using (var stream = new InMemoryRandomAccessStream()) {
       using (var writer = new DataWriter(stream.GetOutputStreamAt(0))) {
          writer.WriteBytes(qrCodeImage);
          await writer.StoreAsync();
       }
       bitmap = new BitmapImage();
       await bitmap.SetSourceAsync(stream);
    }
 
    imageBrush.ImageSource = bitmap;
 
    view.GetReactContext()
        .GetNativeModule<UIManagerModule>()
        .EventDispatcher
        .DispatchEvent(
            new ReactImageLoadEvent(
                view.GetTag(),
                ReactImageLoadEvent.OnLoadStart));
}

4. The final step on native side is to register the ViewManager to the application, this happens in a similar way to NativeModules, via the applications package member function CreateViewManagers

// CustomViewPackage.cs
 
public IReadOnlyList<IViewManager> CreateViewManagers(ReactContext reactContext)
{
    return new List<IViewManager>
    {
        new ReactQrCodeViewManager()
    };
}
 
..
 
// MainReactNativeHost.cs
protected override List<IReactPackage> Packages => new List<IReactPackage>
{
    new MainReactPackage(),
    new CustomViewPackage(),
};

The very final step is to create the JavaScript module that defines the interface between .NET and JavaScript for the users of your new view. Much of the effort is handled by the internal React code in .NET and JavaScript and all that is left for you is to describe the propTypes, that are used for checking the validity of a user’s use of the native view.

// QrCodeView.js
'use strict';
 
var PropTypes = require('prop-types');
var React = require('React');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var ViewPropTypes = require('ViewPropTypes');
var requireNativeComponent = require('requireNativeComponent');
 
class QrCodeView extends React.Component {
    props: {
        data?: string,
        resizeMode?: string
    };
 
    static propTypes = {
        ...ViewPropTypes,
 
        /**
         * Source string for the qrcode.
         */
        data: PropTypes.string,
 
        resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']),
    };
 
    static defaultProps = {
        data: ''
     };
 
     render() {
        return <WindowsQrCodeView {...this.props}/>;
     }
};
 
var WindowsQrCodeView = requireNativeComponent('WindowsQrCodeView', QrCodeView);
 
module.exports = QrCodeView;

Asynchronous communication
React native provides three ways to handle asynchronous communication: callbacks, promises and events.

Callbacks
Native modules support a special kind of argument – a callback. It can be invoked from native code (ie to return the result)

// CustomModule.cs
[ReactMethod]
public void CustomMethodWithCallback (ICallback callback)
{
    Debug.WriteLine(NAME + ": CustomMethodWithCallback called");
    Task.Run(() => {
        Task.Delay(TimeSpan.FromSeconds(1)).Wait();
        callback.Invoke();
    });
}
 
// app.js
import CustomModule from './custom_module'
 
CustomModule.CustomMethodWithCallback (() => console.log('Called back'));

Promises
Native modules can also fulfill a promise. When the last method of a bridged native method is IPromise, the corresponding JS method will return a JS Promise object. This can simplify JS code, using async/await syntax.

[ReactMethod]
public async void canOpenURL(string url, IPromise promise)
{
    if (url == null)
    {
        promise.Reject(new ArgumentNullException(nameof(url)));
        return;
    }
 
    var uri = default(Uri);
    if (!Uri.TryCreate(url, UriKind.Absolute, out uri))
    {
        promise.Reject(new ArgumentException($"URL argument '{uri}' is not valid."));
        return;
    }
 
    try
    {
        var support = await Launcher
            .QueryUriSupportAsync(uri, LaunchQuerySupportType.Uri)
            .AsTask().ConfigureAwait(false);
        promise.Resolve(support == LaunchQuerySupportStatus.Available);
    }
    catch (Exception ex)
    {
        promise.Reject(new InvalidOperationException(
            $"Could not check if URL '{url}' can be opened.", ex));
    }
}
 
// app.js
// The JavaScript counterpart of this method returns a Promise. This means you can use the await keyword within an async function to call it and wait for its result:
async function canOpenUrl(url) {
  try {
    var canOpen = await Launcher.canOpenUrl(url);
    console.log(canOpen);
  } catch (e) {
    console.error(e);
  }
}
 
canOpenUrl('http://foo.bar');

Sending Events
We can send events to JavaScript, without being invoked, using the RTCDeviceEventEmitter attached to the context

// CustomModule.cs
 
private void SendEvent(string eventName, JObject parameters)
{
    Context.GetJavaScriptModule<RCTDeviceEventEmitter>().emit(eventName, parameters);
}
...
SendEvent("customEvent", null);

JavaScript modules can register to receive events by addListener from NativeEventEmitter wrapped module.

// app.js
 
import { NativeEventEmitter } from 'react-native'
 
import CustomModule from './custom_module'
 
export default class App extends Component<{}> {
    componentWillMount() {
        // CustomEventEmitter is the variable name of the event emitter attached to
        // CustomModule
        this.CustomEventEmitter =  new NativeEventEmitter(CustomModule);
        // register CustomEventEmitter to the 'customEvent'
        this.CustomEventEmitter.addListener(
            'customEvent', // <-- event name
            this._handleCustomEvent // <-- callback
          );
    }
 
    _handleCustomEvent = () => {
        console.log('I received a custom event');
    }

Web code reuse
When we started exploring React Native, our hope was to be able to use 100% of our Web app ReactJS code to build a native desktop app. Don’t judge us, we were totally newbie on this technology, and the name sounded close enough! Well, the result was a resounding no. ReactJS is not exactly React Native. Here’s a detailed description of the differences, so you can avoid making the same mistakes we did.

TLDR; we found that the UI syntax has many differences in the base UI components. There are projects whose goal is to have shared codebase between ReactJS and React Native.

Conclusions

On the UI side, React Native was a modern and fun framework to develop on, with a modular architecture by design.

Performance and memory footprint was another advantage over Electron: our prototype used as little as 80MB of RAM, versus 250-300MB of the Electron prototype.

Still, when we explored the technology, it was too new to Windows. Microsoft is currently working to improve performance and stabilize the technology by reimplementing its core in C++, and next milestones are planned for June 2020.

We took the time to investigate new technologies. After exploring React Native and Electron, we will also look into .Net UWP.

Stay tuned!

Sources links:

    Paola Ducolin

    Tech Lead at Dashlane

    Read More