Create a Windows module for React Native with asynchronous code in C#
Published Jun 08 2020 08:39 AM 5,949 Views
Microsoft

We have already explored on this blog the opportunity to create native Windows modules for React Native. Thanks to these modules, you are able to surface native Windows APIs to JavaScript, so that you can leverage them from a React Native application running on Windows. Native modules are one of the many ways supported by React Native to build truly native applications. Despite you're using web technologies, you are able to get access to native features of the underlying platform, like notifications, storage, geolocation, etc.

 

In the previous post we have learned the basics on how to create a module in C# and how to register it in the main application, so that the C# functions are exposed as JavaScript functions. As such, in this post I won't go again into the details on how to build a module and how to register it in the Windows implementation of the React Native app. However, recently, I came across a blocker while working on a customer's project related to this scenario. My post (like the official documentation) was leveraging synchronous methods. In my scenario, instead, I needed to use the Geolocation APIs provided by Windows 10, which are asynchronous and based on the async / await pattern.

 

As such, I've started to build my module using the standard async / await pattern:

 

namespace GeolocationModule
{
    [ReactModule]
    class GeolocationModule
    {
        [ReactMethod("getCoordinates")]
        public async Task<string> GetCoordinates()
        {
            Geolocator geolocator = new Geolocator();
            var position = await geolocator.GetGeopositionAsync();

            string result = $"Latitude: {position.Coordinate.Point.Position.Latitude} - Longitude: {position.Coordinate.Point.Position.Longitude}";

            return result;
        }
    }
}

This is the traditional implementation of this pattern:

  • The GetCoordinates() method is marked with the async keyword
  • The GetCoordinates() method returns Task<T>, where T is the type of the result we want to return (in our case, its' a string)
  • When we call an asynchronous API in the body (the GetGeopositionAsync() method exposed by the Geolocator object), we add the await prefix.

Then, in the React Native portion, I've created a wrapper for this module using the NativeModules APIs, exactly like I did in my previous sample:

 

export const getCoordinates = () => {
  return new Promise((resolve, reject) => {
    NativeModules.GeolocationModule.getCoordinates(function(result, error) {
      if (error) {
        reject(error);
      }
      else {
        resolve(result);
      }
    })
  })
}

Once I've connected all the dots and launched the React Native application, however, I was greeted with an unpleasant surprise. As soon as I hit the button which was invoking the getCoordinates() function, the application was crashing.

 

Thanks to a chat with Vladimir Morozov from the React Native team, it turned out that React Native doesn't support methods which return a Task<T>. In order to work properly, they must return void. How is it possible to achieve this goal and, at the same time, being able to keep calling asynchronous APIs, like the ones exposed by the Geolocator class? Thanks to Vladimir who put me on the right track, the solution is easy. Let's explore the options we have.

Use promises

When it comes to JavaScript, I'm a big fan of promises since they enable a syntax which is very similar to the C# one. When an asynchronous method returns a Promise, you can simply mark the function with the async keyword and add the await prefix before calling the asynchronous function, like in the following sample:

 

const getData = async () => {
    var result = await fetch ('this-is-a-url');
    //do something with the result
}

Let's start to see how we can build our module so that it can return a promise. It's easy, thanks to the IReactPromise interface included in the React Native implementation for Windows. Let's see some code first:

 

using Microsoft.ReactNative.Managed;
using System;
using Windows.Devices.Geolocation;

namespace GeolocationModule
{
    [ReactModule]
    class GeolocationModule
    {
        [ReactMethod("getCoordinatesWithPromise")]
        public async void GetCoordinatesWithPromise(IReactPromise<string> promise)
        {
            try
            {
                Geolocator geolocator = new Geolocator();
                var position = await geolocator.GetGeopositionAsync();

                string result = $"Latitude: {position.Coordinate.Point.Position.Latitude} - Longitude: {position.Coordinate.Point.Position.Longitude}";

                promise.Resolve(result);
            }
            catch (Exception e)
            {
                promise.Reject(new ReactError { Exception = e });
            }
        }
    }
}

As first step, we need to declare the method as async void, in order to be compliant with the React Native requirements. To handle the asynchronous nature of the method, the key component is the requested parameter, which type is IReactPromise<T>, where T is the type of the value we want to return. In my scenario I want to return a string with the full coordinates, so I'm using the IReactPromise<string> type.

 

Inside the method we can start writing our code like we would do in a traditional Windows application. Since we have marked the method with the async keyword, we can just call any asynchronous API (like the GetGeopositionAsync() one exposed by the Geolocator class) by adding the await prefix.

 

Once we have completed the work and we have obtained the result we want to return, we need to pass it to the Resolve() method exposed by the IReactPromise parameter. The method expects a value which type is equal to T. In my case, for example, we would get an error if we try to pass anything but a string.

 

In case something goes wrong, instead, we can use the Reject() method to surface the error to the React Native application, by creating a new ReactError object. You can customize it with different parameters, like Code, Message and UserInfo. In my case, I just want to raise the whole exception, so I simply set the Exception property exposed by my try / catch block.

 

That's it! Now we can easily define a function in our React Native code that, by using the NativeModules API and a Promise, can invoke the C# method we have just created, as in the following sample:

 

const getCoordinatesWithPromise = async () => {
  var coordinates = await NativeModules.GeolocationModule.getCoordinatesWithPromise();
  console.log(coordinates);
}

Since we're using a Promise, the syntax is the same we have seen in the previous sample. We mark the function with the async keyword and we call the getCoordinatesWithPromise() method with the await prefix. Thanks to the Promise we can be sure that, when we invoke the console.log() function, the coordinates property has been properly set. The native module is exposed through the NativeModules APIs and the GeolocationModule object, which is the name of the C# class we have created (if you remember, we have marked it with the [ReactModule] attribute).

 

Use callbacks

I really much prefer the syntax offered by Promises but if, by any chance, you prefer to use callbacks, we got you covered! You can use, in fact, a slightly different syntax, based on the Action<T> object, to expose your native C# method as a callback. Let's see the implementation:

 

using Microsoft.ReactNative.Managed;
using System;
using Windows.Devices.Geolocation;

namespace GeolocationModule
{
    [ReactModule]
    class GeolocationModule
    {
        [ReactMethod("getCoordinatesWithCallback")]
        public async void GetCoordinatesWithCallback(Action<string> resolve, Action<string> reject)
        {
            try
            {
                Geolocator geolocator = new Geolocator();
                var position = await geolocator.GetGeopositionAsync();

                string result = $"Latitude: {position.Coordinate.Point.Position.Latitude} - Longitude: {position.Coordinate.Point.Position.Longitude}";

                resolve(result);
            }
            catch (Exception e)
            {
                reject(e.Message);
            }
        }
    }
}

In this case, as parameters of the method, we are passing two objects which type is Action<T>. The first one will be used when the method completes successfully, the second one when instead something goes wrong. In this case, T is a string in both cases. In case of success, we want to return the usual string with the full coordinates; in case of failure, we want to return the message of the exception.

 

The rest of the code is similar to the one we have written before. The only difference is that, when we have achieved our result, we just pass it to the resolve() function; when something goes wrong, instead, we call the reject() function. The main difference compared to the previous approach is that Action<T> isn't a structured object like the IReactNative<T> interface. As such, it doesn't support to pass the full exception but, in this case, we choose to pass only the Message property of the exception.

 

That's if from the C# side. Let's move to the JavaScript one. In this case, being a callback, we can't use anymore the async and await keywords, but we need to pass to the method two functions: one will be called when the operation is successful, one when we get an error.

 

const getCoordinatesWithCallback = () => {
  NativeModules.GeolocationModule.getCoordinatesWithCallback( (result) => { console.log(result); }, (error) => console.log(error));
}

 

Passing parameters

In both scenarios, if we need to pass any parameter from the JavaScript code to the C# one we can just add them before the IReactPromise<T> or the Action<T> ones. For example, let's say we want to set the desired accuracy of the Geolocator object with a value in meters, passed from the React Native app. We can just define the method like this:

 

[ReactMethod("getCoordinatesWithPromise")]
public async void GetCoordinatesWithPromise(uint meters, IReactPromise <string> promise)
{
    try
    {
        Geolocator geolocator = new Geolocator();
        geolocator.DesiredAccuracyInMeters = meters;
        
       var position = await geolocator.GetGeopositionAsync();

        string result = $"Latitude: {position.Coordinate.Point.Position.Latitude} - Longitude: {position.Coordinate.Point.Position.Longitude}";

        promise.Resolve(result);
    }
    catch (Exception e)
    {
        promise.Reject(new ReactError { Exception = e });
    }
}

Then, from JavaScript, we just need to pass the value of the parameter when we invoke the getCoordinatesWithPromise() function:

 

const getCoordinatesWithPromise = async () => {
  var coordinates = await NativeModules.GeolocationModule.getCoordinatesWithPromise(15);
  console.log(coordinates);
}

Of course, you can use the same pattern if you prefer to leverage the callback approach.

 

Wrapping up

On this blog we have already learned how to build native C# modules for React Native so that, as a developer, you can access native Windows APIs from JavaScript. However, in that sample we leveraged only synchronous APIs. The reality, however, is that most of the Windows 10 APIs are implemented with the asynchronous pattern. Unfortunately, as soon as you start using them, you will face errors and crashes, since React Native isn't able to properly understand the traditional async / await pattern, based on the Task object. Thanks to the React Native team I've been pointed into the right direction, by leveraging the IReactNative interface (in case you want to enable asynchronous APIs with promises) or the Action object (in case you want to leverage the more traditional callback approach).

 

Regardless of the approach you prefer, you'll be able to achieve your goal of leveraging asynchronous Windows 10 APIs from your React Native application running on Windows:

 

FinalApp.png

3 Comments
Version history
Last update:
‎Jun 09 2020 09:37 AM
Updated by: