React Native JSI: How to expose a native object to javascript code

1.7k views Asked by At

I use RN 0.66.3 and want to make direct sync calls from javascript to native code in my iOS React Native project to share data without using the React Native Bridge for performance purposes so that I need to have a shared global object and access to its properties and methods from javascript.

I know that is possible with JSI (JavaScript Interface) but there are no docs and few tutorials about so what the simple steps or sample code to implement this?

1

There are 1 answers

0
iUrii On

To expose your object to javascript over React Native JSI you should make next steps:

  1. Make your c++ class inherited from HostObject
  2. Override get and set methods to implement access to its properties and methods.
  3. Install your object globally on React Native runtime.

Look at NativeStorage sample that can store key/value pairs persistently to NSUserDefaults across launches of your app:

NativeStorage class

#include <jsi/jsi.h>
#import <React/RCTBridge+Private.h>

using namespace facebook::jsi;
using namespace std;

// Store key-value pairs persistently across launches of your app.
class NativeStorage : public HostObject {
public:
  /// Stored property
  int expirationTime = 60 * 60 * 24; // 1 day
  
  // Helper function
  static NSString* stringValue(Runtime &runtime, const Value &value) {
    return value.isString()
      ? [NSString stringWithUTF8String:value.getString(runtime).utf8(runtime).c_str()]
      : nil;
  }
  
  Value get(Runtime &runtime, const PropNameID &name) override {
    auto methodName = name.utf8(runtime);
    
    // `expirationTime` property getter
    if (methodName == "expirationTime") {
      return this->expirationTime;
    }
    // `setObject` method
    else if (methodName == "setObject") {
      return Function::createFromHostFunction(runtime, PropNameID::forAscii(runtime, "setObject"), 2,
                                                        [](Runtime &runtime, const Value &thisValue,const Value *arguments, size_t count) -> Value {
        NSString* key = stringValue(runtime, arguments[0]);
        NSString* value = stringValue(runtime, arguments[1]);
        if (key.length && value.length) {
          [NSUserDefaults.standardUserDefaults setObject:value forKey:key];
          return true;
        }
        return false;
      });
    }
    // `object` method
    else if (methodName == "object") {
      return Function::createFromHostFunction(runtime, PropNameID::forAscii(runtime, "object"), 1,
                                                        [](Runtime &runtime, const Value &thisValue,const Value *arguments, size_t count) -> Value {
        NSString* key = stringValue(runtime, arguments[0]);
        NSString* value = [NSUserDefaults.standardUserDefaults stringForKey:key];
        return value.length
          ? Value(runtime, String::createFromUtf8(runtime, value.UTF8String))
          : Value::undefined();
      });
    }
    return Value::undefined();
  }
  
  void set(Runtime& runtime, const PropNameID& name, const Value& value) override {
    auto methodName = name.utf8(runtime);
    
    // ExpirationTime property setter
    if (methodName == "expirationTime") {
      if (value.isNumber()) {
        this->expirationTime = value.asNumber();
      }
    }
  }
  
  // Install `nativeStorage` globally to the runtime
  static void install(Runtime& runtime) {
    NativeStorage nativeStorage;
    shared_ptr<NativeStorage> binding = make_shared<NativeStorage>(move(nativeStorage));
    auto object = Object::createFromHostObject(runtime, binding);

    runtime.global().setProperty(runtime, "nativeStorage", object);
  }
};

AppDelegate.mm

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  
  // Runtime notification
  [NSNotificationCenter.defaultCenter addObserverForName:RCTJavaScriptDidLoadNotification object:nil queue:nil
                                              usingBlock:^(NSNotification* notification) {
    RCTCxxBridge* cxxbridge = (RCTCxxBridge*)notification.userInfo[@"bridge"];
    if (cxxbridge.runtime) {
      NativeStorage::install(*(Runtime*)cxxbridge.runtime);
    }
  }];
  
  return YES;
}

App.js

nativeStorage.expirationTime = 1000;
console.log(nativeStorage.expirationTime);

const key = "greeting";
nativeStorage.setObject(key, "Hello JSI!");
const text = nativeStorage.object(key);
console.log(text);

Outputs:

1000
Hello JSI!

Future React Native's TurboModules & CodeGen makes all of this cleaner & easier but it's the low level JSI implementation of the native module that can be called directly from JavaScript without going through the React Native Bridge.

Note: Since the sample uses JSI for synchronous native methods access, remote debugging (e.g. with Chrome) is no longer possible. Instead, you should use Flipper for debugging your JS code.