Bridging the Gap: How to Call Native Component Functions from ReactNative

Suson Thapa
7 min readMar 16, 2023
Photo by Aveedibya Dey on Unsplash

If you have built a native component in ReactNative then you probably know that you can pass props to change the behavior of the component.

Have you ever had a requirement to call a function on the native component itself?

If you do a quick google search you will run into different workaround people have developed over the years and the ReactNative documentation doesn’t help at all.

In this blog, I will try to explain two approaches that we can use to call the native component functions. For developing the native component I will be using Kotlin for Android and Swift for iOS.

What will we be building?

We will approach this with the goal of making the implementations similar on both Android and iOS. We will be building a very simple app to illustrate the different approaches.

The red view in the app is the native component that we will be using to illustrate the two approaches for calling the function on it.

Exposing ViewManagers as NativeModules

Let’s get it straight, it is quite easy to expose native component functions on iOS. I don’t know how much of it is due to the flexibility of objective-c. Here is the native code for the red box on iOS.

TestView.swift

class TestView: UIView {

override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

@objc var testProp: String = "" {
didSet {
print("setTestProp: value -> \(testProp)")
}
}

}

TestViewManager.swift

@objc(TestViewManager)
class TestViewManager: RCTViewManager {
override func view() -> UIView! {
return TestView()
}

@objc func testMethod(_ tag: Int, _ value: String) {
print("testMethod: tag -> \(tag), value -> \(value)")
}

override class func requiresMainQueueSetup() -> Bool {
true
}

}

TestViewManager.m

@interface RCT_EXTERN_REMAP_MODULE (TestView, TestViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(testProp, NSString)
RCT_EXTERN_METHOD(testMethod
: (NSNumber *)tag
: NSString)
@end

With the above code, we can access the TestViewManager as a native component as well as a native module. By default, the NativeModule will be exposed as TestViewManager so we are using RCT_EXTERN_REMAP_MODULE to expose it as TestView to ReactNative.

The following code will work perfectly on iOS.

const TestView = requireNativeComponent('TestView');

function App(): JSX.Element {

const testRef = useRef()

useEffect(() => {
NativeModules.TestView.testMethod(findNodeHandle(testRef.current), 'Hello World')
}, [])

return (
<SafeAreaView>
<StatusBar />
<View style={{
width: 100,
height: 100,
}}>

<TestView ref={testRef} testProp='Susan Thapa' style={{
flex: 1,
}} />

</View>
</SafeAreaView>
);
}

We can access the TestView NativeComponent as well as TestView NativeModule and call functions on it.

What about Android?

Let’s build the same component on Android. The following code is a pretty standard way to expose a native component.

TestView.kt

@SuppressLint("ViewConstructor")
class TestView(context: ReactContext) : FrameLayout(context) {
init {
setBackgroundColor(Color.RED)
}
}

TestViewManager.kt

class TestViewManager : SimpleViewManager<TestView>() {
override fun getName(): String {
return "TestView"
}

override fun createViewInstance(reactContext: ThemedReactContext): TestView {
return TestView(reactContext)
}

@ReactProp(name = "testProp")
fun setTestProp(view: TestView, prop: String) {
Log.d(TAG, "setTestProp: value -> $prop")
}

@ReactMethod
fun testMethod(tag: Int, value: String) {
Log.d(TAG, "testMethod: tag -> $tag, value -> $value")
}

companion object {
const val TAG = "TestViewManager"
}
}

TestViewPackage.kt

class TestViewPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return mutableListOf()
}

override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<*, *>> {
return mutableListOf(TestViewManager())
}
}

Let’s try to run it.

On Android, ReactNative can’t find the testMethod even if we annotate with @ReactMethod .

Well, ReactNative doesn’t expose ViewManagers as NativeModules on Android as it does on iOS.

However, if you look into the ViewManager class it extends BaseJavaModule which means a ViewManager is technically a NativeModule.

So, we can expose the ViewManager as both ViewManager and NativeModule in the package class.

class TestViewPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return mutableListOf(TestViewManager())
}

override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<*, *>> {
return mutableListOf(TestViewManager())
}
}

And the app works!

Any downside to this approach?

Well, there is really one big downside. If you debug the app, you will see that these functions are called on a background thread.

Android
iOS

If you want to access the view in this function then you have to be careful as the view is not guaranteed to be initialized. Also, you have to switch to the main thread to access the view.

On Android, you can use runOnUiQueueThread to run the code on the main thread and use UIManagerHelper to resolve the view.

    @ReactMethod
fun testMethod(tag: Int, value: String) {
reactContext.runOnUiQueueThread {
val view = UIManagerHelper.getUIManagerForReactTag(reactContext, tag)?.resolveView(tag)
}
Log.d(TAG, "testMethod: tag -> $tag, value -> $value")
}

On iOS, you can use DispatchQueue to run it on the main thread and use the bridge to resolve the view.

  @objc func testMethod(_ tag: NSNumber, _ value: String) {
DispatchQueue.main.async { [weak self] in
let view = self?.bridge.uiManager.view(forReactTag: tag)
}
print("testMethod: tag -> \(tag), value -> \(value)")
}

If you call this function in the componentDidMount or useEffect and that function accesses the view then you might run into a race condition where the createViewInstance (Android) or view (iOS) might not have been called which will result in a crash.

However, if you are intending to call these functions based on some user interactions then you can be fairly certain that the view will be there.

Dispatching commands

This is another approach that you can use to call the native component functions. This is the preferred approach as it guarantees that the view is created before those functions are executed. The implementation will be slightly different on Android and iOS.

The only downside of this approach is you can’t use callbacks or promises. But let's be honest, if you need to use callbacks or promises then you are better off creating a separate NativeModule.

For iOS, we have to slightly change the implementation. As the function is called on a background thread, ReactNative recommends running the function code within an UIBlock to run it on the main thread.

  @objc func testMethod(_ tag: NSNumber, _ value: String) {
self.bridge.uiManager.addUIBlock {_, viewRegistry in
let view = viewRegistry?[tag] as? TestView
view?.backgroundColor = .cyan
}
print("testMethod: tag -> \(tag), value -> \(value)")
}

On Android, you have to override receiveCommands functions like this. This function receives the view as an argument so we don’t have to resolve it anymore and it is called on the main thread.

    override fun receiveCommand(root: TestView, commandId: String?, args: ReadableArray?) {
super.receiveCommand(root, commandId, args)
when (commandId) {
"testMethod" -> testMethod(root, args?.getString(0) ?: "")
}
Log.d(TAG, "receiveCommand: command: $commandId, args: $args")
}

fun testMethod(view: TestView, value: String) {
view.setBackgroundColor(Color.CYAN)
Log.d(TAG, "testMethod: value -> $value")
}

Then you can call the function from ReactNative like this.

UIManager.dispatchViewManagerCommand(findNodeHandle(testRef.current), "testMethod", ['Hello World'])

Comment down below if you have any other way to call the component functions.

Bonus

Here is a bonus for you. If you want to access the React Native view from a native module you can use the following code.

Pass React Native View Tag

You can get the native view tag from a React reference like this.

const viewTag = findNodeHandle(ref)

Get the Native View on Android

Use UIManagerHelper class to resolve the view. You need to run this code from UIQueueThread which can be done like this.

    runOnUiQueueThread {
try {
val manager = UIManagerHelper.getUIManagerForReactTag(reactContext, tag)
val view = manager?.resolveView(tag)
if (view == null) {
Log.d(TAG, "findView: view with tag $tag not found")
return
}
if (view is YOUR_VIEW_TYPE) {
// Do something with the view
} else {
Log.d(TAG, "findView: view is not of type YOUR_VIEW_TYPE")
}
} catch (e: Exception) {
e.printStackTrace()
}
}

Get the Native View on iOS

Use UIManager class on iOS to resolve the view. You need to run this code from MainThread which can be done like this.

    DispatchQueue.main.async {
if let view = bridge.uiManager.view(forReactTag: tag) as? YOUR_VIEW_TYPE {
// do something with the view
} else {
RCTLog("findView: Failed to find the view with tag \(tag)")
}
}

That’s it for this story. Catch you on the next one, peace.

--

--

Suson Thapa

Android | iOS | Flutter | ReactNative — Passionate Software Engineer