c++iosobjective-creact-nativereact-native-jsi

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


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?


Solution

  • 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.