'Native module in React Native: function returns "undefined"

So, I am practicing making native modules in Java that can have functions exposed to the JavaScript code of React Native. For that, I decided to go for a simple demo math library. My code is as follows.

MathOpsModule.java

package com.mb_rn_poc;

import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class MathOpsModule extends ReactContextBaseJavaModule {

  @Override
  public String getName() {
    return "MathOps"; // Name of the Native Modules.
  }

  @ReactMethod
  public int add(int a, int b) {
    return (a + b);
  }
}

MathOpsPackage.java

package com.mb_rn_poc;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class MathOpsPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(
  ReactApplicationContext reactContext
) {
    return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(
      ReactApplicationContext reactContext
) {
    List<NativeModule> modules = new ArrayList<>();
    // Register the MathOps module
    modules.add(new MathOpsModule());
    return modules;
  }
}

MainApplication.java

package com.mb_rn_poc;

import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
      new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
          return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
          @SuppressWarnings("UnnecessaryLocalVariable")
          List<ReactPackage> packages = new PackageList(this).getPackages();
          // Packages that cannot be autolinked yet can be added manually here, for example:
          // packages.add(new MyReactNativePackage());
          packages.add(new MathOpsPackage());
          return packages;
        }

        @Override
        protected String getJSMainModuleName() {
          return "index";
        }
      };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
  }

  /**
   * Loads Flipper in React Native templates. Call this in the onCreate method with something like
   * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
   *
   * @param context
   * @param reactInstanceManager
   */
  private static void initializeFlipper(
      Context context, ReactInstanceManager reactInstanceManager) {
    if (BuildConfig.DEBUG) {
      try {
        /*
         We use reflection here to pick up the class that initializes Flipper,
        since Flipper library is not available in release mode
        */
        Class<?> aClass = Class.forName("com.mb_rn_poc.ReactNativeFlipper");
        aClass
            .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
            .invoke(null, context, reactInstanceManager);
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    }
  }
}

Now, on the React Native side of things, I have these two files -

MathOps.js

import { NativeModules } from 'react-native';

const MathOps = NativeModules.MathOps;

export const add = (a, b) => {
    return MathOps.add(a, b);
}

MainApp.js

import React, { useEffect, useState } from 'react';
import { SafeAreaView, Text, View } from 'react-native';
import { add } from "./NativeWrapper/MathOps";

const MainApp = () => {

    const [state, setState] = useState(0);

    useEffect(() => {
        let sum = add(10, 12);
        console.log({ sum });
        setState(sum);

    }, [])

    return (
        <SafeAreaView>
            <View>
                <Text>
                    {state}
                </Text>
            </View>
        </SafeAreaView>
    );
}

export default MainApp;

The problem is, the add function is returning undefined. As such, nothing gets printed on the screen. Any idea what I might be doing wrong?



Solution 1:[1]

Calls to native methods are asynchronous, so they cannot return values directly.

You can either use a callback (as you figured out yourself), or a Promise to allow for the more elegant async / await syntax (and more):

@ReactMethod
public void add(Double a, Double b, Promise promise) {
    Double sum = a + b;
    promise.resolve(sum);
}
import { NativeModules } from 'react-native';

const { MathOps } = NativeModules;

export const add = async (a, b) => {
    return await MathOps.add(a, b);
}

Solution 2:[2]

Not sure what the issue with this is, but there is a workaround - using Callbacks, as described in https://reactnative.dev/docs/native-modules-android. I made the following modifications -

@ReactMethod
public void add(Double a, Double b, Callback cb) {
    Double sum = a + b;
    cb.invoke(sum);
}
import { /* ... */ NativeModules } from 'react-native';
const { MathModule } = NativeModules;

// ...
// ...

MathModule.add(10, 20, (sum) => {
    alert(sum);
})

And it does what I expect it to - alerts "30".

Solution 3:[3]

React Native bridge is asynchronous. We can pass data from Native Side to Javascript in following ways:-

1) Promise: When the last parameter of a native module Java/Kotlin method is a Promise, its corresponding JS method will return a JS Promise object.

In Module.java->

@ReactMethod
public void add(Double a, Double b, Promise promise) {            
        Double sum = a + b;
        promise.resolve(sum);
}       

This is how you will then access in Javascript->

import { NativeModules } from 'react-native'; 
const { MathOps } = NativeModules;

export const add = async (a, b) => {
    return await MathOps.add(a, b);
}

2. Callback: They can also be used to asynchronously execute JavaScript from the native side.

In Module.java->

@ReactMethod
public void add(Double a, Double b, Callback cb) {            
        Double sum = a + b;
        cb.invoke(sum);
} 

This is how you will then access in Javascript->

import { NativeModules } from 'react-native'; 
const { MathOps } = NativeModules;

useEffect(() => {
  MathOps.add(10, 12, (sum) => {
    setState(sum);
  });
}, [])

3. isBlockingSynchronousMethod (Not Recommended due to performance penalties and threading-related bugs to your native modules): You can pass isBlockingSynchronousMethod = true to a native method to mark it as a synchronous method.

@ReactMethod(isBlockingSynchronousMethod = true)
public Double add(Double a, Double b){
    Double sum = a + b;
    return sum;
}

In js file:

import { NativeModules } from 'react-native'; 
const { MathOps } = NativeModules;

export const add = async (a, b) => {
    return await MathOps.add(a, b);
}

4. Emitting Events: haven't explored it much.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 kraxor
Solution 2 Debadeep Sen
Solution 3 mohit arora