8

It's clear to me that for react-native native modules we can use the @ReactMethod to export a method and call it from JSX, but how do we do the same thing in react-native native UI components?

In the documentation I only see @ReactProp being mentioned. If @ReactMethod is not working, how do I access a property of my native UI component from JSX then? (On iOS this can be done on native ui components with RCT_EXPORT_METHOD but on Android is something similar possible?)

Thank you.

SudoPlz
  • 20,996
  • 12
  • 82
  • 123

2 Answers2

12

Ok I ended up creating a Module, and passing a UI Component reference on its constructor:

Here's my UI component:

public class RCTACCalendarManager extends ViewGroupManager<RCTACCalendar> {
    public static final String REACT_CLASS = "RCTACCalendar";
    private RCTACCalendar mCalendarInstance;

    public RCTACCalendarManager(ReactApplicationContext reactContext) {
        super();
    }

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    public RCTACCalendar createViewInstance(ThemedReactContext context) {
        mCalendarInstance = new RCTACCalendar(context);
        return mCalendarInstance;
    }

    public RCTACCalendar getCalendarInstance() { // <-- returns the View instance
        return mCalendarInstance;
    }
}

Here's the Module I created for that component:

public class RCTACCalendarModule extends ReactContextBaseJavaModule {
    private RCTACCalendar mCalendarInstance;

    public RCTACCalendarModule(ReactApplicationContext reactContext, RCTACCalendarManager calManager) {
        super(reactContext);
        if (calManager != null) {
            mCalendarInstance = calManager.getCalendarInstance();
        }
    }

    @Override
    public String getName() {
        return "ACCalendarManager";
    }

    @ReactMethod
    public void mySuperDuperFunction(Promise promise) {
        if (mCalendarInstance != null) {
            mCalendarInstance.mySuperDuperFunction(promise); // <-- Magic
        }
    }
}

and here's how I combine those two together in my Package declaration:

public class RCTACCalendarPackage implements ReactPackage {
    private RCTACCalendarManager mCalManager;

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        if (mCalManager == null) {
            mCalManager = new RCTACCalendarManager(reactContext);
        }
        return Arrays.<NativeModule>asList(new RCTACCalendarModule(reactContext, mCalManager));
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        if (mCalManager == null) {
            mCalManager = new RCTACCalendarManager(reactContext);
        }
        return Arrays.<ViewManager>asList(mCalManager);
    }
}

It works like a charm.

friederbluemle
  • 33,549
  • 14
  • 108
  • 109
SudoPlz
  • 20,996
  • 12
  • 82
  • 123
  • I'm trying your method. Android can not export a method and then use `NativeModules.YourNativeModule` to get the method in js layer. It's really not cool! – Bruce Lee May 14 '18 at 09:10
  • 1
    Stellar answer. Would have taken me ages to work this out myself. – Adamski May 15 '18 at 12:07
  • public class RCTACCalendarModule ...... private RCTACCalendarManager calendarManager; public RCTACCalendarModule(ReactApplicationContext reactContext, RCTACCalendarManager calManager) { super(reactContext); calendarManager = calManager; }........ @ReactMethod public void mySuperDuperFunction(Promise promise) { calendarInstance = calendarManager. getCalendarInstance() if (calendarManager != null) { calendarInstance.mySuperDuperFunction(promise); } } – Nick Apr 07 '21 at 17:54
  • Genius! I have struggled for a whole day trying to make it work through UIManager. Using your way, I have been able to implement it in 5 minutes without any issue. Thank you! One thing though. I'm not caching a pointer to the view, but to the viewManager. In my case view is nil at the time when Module is initialised. – Eugene Alexeev Jul 01 '21 at 15:00
  • 1
    Your solution allows only one instance of the view at the same time. IMHO the answer by Nick is better as it is allowing several instances. – Christian Aug 31 '21 at 10:12
10

The existing answer works great if you only need to support Android, but I found it didn't work when I was trying to integrate with iOS as well. I burnt quite a lot of time trying to wrangle this method into iOS, so I'll recommend what I came up with instead: using the UIManager that comes with react-native.

React Native Component

// ComponentWithNativeFunctionality.js

import {UIManager, findNodeHandle} from 'react-native';

class ComponentWithNativeFunctionality extends React.Component {
  const myRef = React.createRef();

  functionToCall = () => {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.myRef.current),
      "nameOfFunctionToCallInNativeLand",
      [/* additional arguments */]
    );
  }

  render() {
    return <NativeComponentView ref={this.myRef} />
  }
}

Android

// YourViewManager.java

public class YourViewManager extends SimpleViewManager<YourView> {
    // ...

    @Override
    public void receiveCommand(@NonNull YourView view, String commandId, @Nullable ReadableArray args) {
        super.receiveCommand(view, commandId, args);
            switch (commandId) {
                case "nameOfFunctionToCallInNativeLand":
                    view.nameOfFunctionToCallInNativeLand();
                    break;
            }
        }
    }
}

iOS (with Swift)

  1. Add #import "React/RCTUIManager.h" to your Bridging-Header.h
// YourViewManager.m

@interface RCT_EXTERN_MODULE(YourViewManagerClass, RCTViewManager)

//...

RCT_EXTERN_METHOD(
    nameOfFunctionToCallInNativeLand: (nonnull NSNumber *)node
)

@end
// YourViewManagerClass.swift

@objc(YourViewManagerClass)
class YourViewManagerClass: RCTViewManager {
    @objc func nameOfFunctionToCallInNativeLand(_ node: NSNumber) -> Void {
        DispatchQueue.main.async {
            let component = self.bridge.uiManager.view(
              forReactTag: node
            ) as! MisnapCameraView
            component.nameOfFunctionToCallInNativeLand()
        }
    }
}

Another note: you can't pass in a Promise like you can with modules. You will have to pass in a unique ID generated in JS, and then once your action is done, fire an event to bubble back the result to JS with the ID attached to the event.

friederbluemle
  • 33,549
  • 14
  • 108
  • 109
Josh Baker
  • 598
  • 7
  • 16
  • This is IMHO superior to the accepted answer as it allows two instances of the same View. – Christian Aug 31 '21 at 10:11
  • I was doing a deep dive into ReactNative and I can say that this is the correct approach, `dispatchViewManagerCommand` has some internal logic to validate that the view has been initialized. When I tried to use the same `ViewManager` as a module on Android I ran into situations where the view wasn't initialized sometimes. – Susan Thapa Mar 10 '23 at 11:51