React-native: Illegal callback invocation from native module error

Created on 20 Apr 2017  路  9Comments  路  Source: facebook/react-native

Description

After upgraded to 0.41, many modules don't work and get this error:

java.lang.RuntimeException: Illegal callback invocation from native module. This callback type only permits a single invocation from native code.
at com.facebook.react.bridge.CallbackImpl.invoke(CallbackImpl.java:32)

I think it's caused by this change in react-native which limits the callback to be invoked for only once:

Fail-Fast on Redundant Java Callback Invocations

  @Override
  public void invoke(Object... args) {
+    if (mInvoked) {
+      throw new RuntimeException("Illegal callback invocation from native "+
+        "module. This callback type only permits a single invocation from "+
+        "native code.");
+    }
    mCatalystInstance.invokeCallback(mExecutorToken, mCallbackId, Arguments.fromJavaArgs(args));
+    mInvoked = true;
  }

However, many modules would expect their callbacks to be invoked more than once. E.g. react-native-dialogs which shows native dialogs for Android. And on the dialog, we will probably invoke the callback for multiple times, e.g. select an option, press ok will both invoke the callback with different args.

Have we considered that sometimes we do need multiple callback invocations?

Stale

Most helpful comment

Instead of using callbacks, use events with RCTEventEmitter

Example below using swift. I left out the .m since it isn't relevant to event emitting with swift. The obj-c version is about the same and will be easy for you to google/figure out

edit: I put together a small blog post to explain this: https://medium.com/nycdev/calling-a-callback-multiple-times-in-a-react-native-module-5c3c61f2fca4

.swift

@objc(RNSpeechToText)
class RNSpeechToText: RCTEventEmitter {

  var speechToText: SpeechToText?
  var audioPlayer = AVAudioPlayer()
  var hasListeners = false

  static let sharedInstance = RNSpeechToText()

  private override init() {}

  override func supportedEvents() -> [String]! {
    return ["StreamingText"]
  }

  @objc func initialize(_ username: String, password: String) -> Void {
    speechToText = SpeechToText(username: username, password: password)
  }

  @objc func startStreaming(_ errorCallback: @escaping RCTResponseSenderBlock) {

    var settings = RecognitionSettings(contentType: .opus)
    settings.interimResults = true

    let failure = { (error: Error) in errorCallback([error]) }

    speechToText?.recognizeMicrophone(settings: settings, failure: failure) { results in
      if(self.hasListeners)
      {
        self.sendEvent(withName: "StreamingText", body: results.bestTranscript)
      }
    }
  }

  @objc func stopStreaming() {
    speechToText?.stopRecognizeMicrophone()
  }

  override func startObserving()
  {
    hasListeners = true
  }

  override func stopObserving()
  {
    hasListeners = false
  }
}

.js

import { NativeEventEmitter, NativeModules } from 'react-native';

let {
    RNSpeechToText
} = NativeModules

module.exports = {
SpeechToText: {
        speechToTextEmitter: new NativeEventEmitter(RNSpeechToText),

        initialize: function ( username, password )
        {
            RNSpeechToText.initialize( username, password );
        },

        startStreaming(callback)
        {
            this.subscription = this.speechToTextEmitter.addListener(
                'StreamingText',
                (text) => callback(null, text)
            );

            RNSpeechToText.startStreaming(callback)
        },

        stopStreaming()
        {
            this.subscription.remove()

            RNSpeechToText.stopStreaming()
        }
    }
}

All 9 comments

This appears to be the same issue as https://github.com/facebook/react-native/issues/12729. What's the answer guys?

Ok, here is the answer for now: Whenever you invoke a method from the javascript side on the module that may in turn invoke the callback you need to reset the callback:

resetCallback(){
MyModule.setTheCallback((value) => {
this.setState({ value: value });
});
}
invokeMethodThatMayInvokeCallback(){
resetCallback();
MyModule.bar(); // the method within the module that will invoke the callback.
}

@gavinyeah did you find any solution on this? I have the same problem on a single options dialog. The second time I try to select an option the app crash. Is this work around from @dobrienlzd working with the react-native-dialogs lib? If it's so I can't figure out how.

EDIT: seems like setting alwaysCallSingleChoiceCallback to false will solve my particular issue with the lib

Facing the same issue

I'm with this issue also

Instead of using callbacks, use events with RCTEventEmitter

Example below using swift. I left out the .m since it isn't relevant to event emitting with swift. The obj-c version is about the same and will be easy for you to google/figure out

edit: I put together a small blog post to explain this: https://medium.com/nycdev/calling-a-callback-multiple-times-in-a-react-native-module-5c3c61f2fca4

.swift

@objc(RNSpeechToText)
class RNSpeechToText: RCTEventEmitter {

  var speechToText: SpeechToText?
  var audioPlayer = AVAudioPlayer()
  var hasListeners = false

  static let sharedInstance = RNSpeechToText()

  private override init() {}

  override func supportedEvents() -> [String]! {
    return ["StreamingText"]
  }

  @objc func initialize(_ username: String, password: String) -> Void {
    speechToText = SpeechToText(username: username, password: password)
  }

  @objc func startStreaming(_ errorCallback: @escaping RCTResponseSenderBlock) {

    var settings = RecognitionSettings(contentType: .opus)
    settings.interimResults = true

    let failure = { (error: Error) in errorCallback([error]) }

    speechToText?.recognizeMicrophone(settings: settings, failure: failure) { results in
      if(self.hasListeners)
      {
        self.sendEvent(withName: "StreamingText", body: results.bestTranscript)
      }
    }
  }

  @objc func stopStreaming() {
    speechToText?.stopRecognizeMicrophone()
  }

  override func startObserving()
  {
    hasListeners = true
  }

  override func stopObserving()
  {
    hasListeners = false
  }
}

.js

import { NativeEventEmitter, NativeModules } from 'react-native';

let {
    RNSpeechToText
} = NativeModules

module.exports = {
SpeechToText: {
        speechToTextEmitter: new NativeEventEmitter(RNSpeechToText),

        initialize: function ( username, password )
        {
            RNSpeechToText.initialize( username, password );
        },

        startStreaming(callback)
        {
            this.subscription = this.speechToTextEmitter.addListener(
                'StreamingText',
                (text) => callback(null, text)
            );

            RNSpeechToText.startStreaming(callback)
        },

        stopStreaming()
        {
            this.subscription.remove()

            RNSpeechToText.stopStreaming()
        }
    }
}

@pwcremin I tried following this method to fix this issue and having some serious issues getting NativeEventEmitter to work

Our events are not being registered and are receiving this error message in xcode: 'TestEvent' is not a supported event type for RCTEventEmitter. Supported events are: '(null)'

We have reviewed the documentation on how to implement a NativeEventEmitter and I believe we are following it pretty closely. The problem I think is with Objective-C and how we are including our EventManager class, the JavaScript side of this is really simple.

// SpotifyLoginViewController.m

#import "SpotifyLoginViewController.h"
#import "AppDelegate.h"
#import "SpotifyAuth.h"

@interface SpotifyLoginViewController () <WKNavigationDelegate>
@property (strong, nonatomic) WKWebView *webView;
@property(nonatomic) NSURL *login;

@end

@implementation SpotifyLoginViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    _webView = [[WKWebView alloc] initWithFrame:self.view.frame
                                configuration:configuration];
    _webView.navigationDelegate = self;
    _webView.allowsBackForwardNavigationGestures = true;
    [[NSURLCache sharedURLCache] removeAllCachedResponses];

    [_webView loadRequest:[NSURLRequest requestWithURL:_login]];
    self.view = _webView;

}

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
      id<RCTBridgeDelegate> module = [[EventManager alloc] init];
      RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:module launchOptions:nil];

      SpotifyAuth *sharedManager = [SpotifyAuth sharedManager];
      NSURL *url = navigationAction.request.URL;
      NSString *myScheme = [[sharedManager myScheme] stringByReplacingOccurrencesOfString:@"://callback" withString:@""];
      if ([url.scheme isEqualToString:myScheme]) {

        RCTEventEmitter *event = [[RCTEventEmitter alloc] init];
        event.bridge = bridge;
        [event sendEventWithName:@"TestEvent" body:@{@"name":@"metallica"}];

        decisionHandler(WKNavigationActionPolicyCancel);
        return;
      }
      decisionHandler(WKNavigationActionPolicyAllow);
    }

// EventManager.h

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>


@interface EventManager : RCTEventEmitter <RCTBridgeModule>

- (NSURL*)sourceURLForBridge:(RCTBridge*)bridge;

@end

// EventManager.m

#import "EventManager.h"
#import <React/RCTBundleURLProvider.h>

@implementation EventManager

RCT_EXPORT_MODULE();

- (NSArray<NSString *> *)supportedEvents {
  return @[@"TestEvent"];
}

- (NSURL*)sourceURLForBridge:(RCTBridge*)bridge {
  NSURL *jsCodeLocation;
  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
  return jsCodeLocation;  // [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
}

@end

// player.js

import { NativeModules, NativeEventEmitter } from 'react-native';
const eventManager = new NativeEventEmitter(NativeModules.EventManager);

subscription = eventManager.addListener('TestEvent', (data) => {
  console.log('the event fired!');
  console.log(data);
});

We are using RN 0.44.0

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions.

Hi there, still happening on 0.52.1.

As says in the docs:

A native module is supposed to invoke its callback only once. It can, however, store the callback and invoke it later.

Was this page helpful?
0 / 5 - 0 ratings