Logo
Published on
·11 min read

Resolving Warnings and Errors When Using react-native-tts

I apologize in advance for any awkward expressions in English. 🙏

English is not my native language, and I have relied on ChatGPT's assistance to proceed with the translation.

Introduction

I have decided to add Text-to-Speech (TTS) functionality to my ongoing React Native project, and I've chosen to use the react-native-tts library for this purpose.

For detailed information about the library, you can refer to react-native-tts.

After installing the library and testing it with the code Tts.speak('Hello, world!');, I confirmed that the speech functionality is working as expected. However, I encountered a warning at the app's startup.

Warning

 WARN  `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method.
 WARN  `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method.

Identifying the Cause

Based on information I found on Stack Overflow, it is likely that the issue is caused by using the latest version of React Native with the react-native-tts library, which lacks the required addListener and removeListeners methods for the latest React Native version.

Checking the Library

The version I had installed was 4.1.0, while the latest version of react-native-tts was 4.1.1 as of December 20, 2023.

I attempted to update the library with the command npm i react-native-tts@4.1.1, but it resulted in an error indicating that it could not be found.

To explore other solutions, I decided to inspect the library's code.

Checking the Library Code

I found that the library's code, specifically in the Git > TextToSpeechModule.java file, includes the missing addListener and removeListeners methods that were not present in the module I had installed. Here's how they are implemented:

    @ReactMethod
    public void removeListeners(Integer count) {
        // Keep: Required for RN built in Event Emitter Calls.
    }

    @ReactMethod
    public void addListener(String eventName) {
        // Keep: Required for RN built in Event Emitter Calls.
    }

Solution

I added the above code directly to my installed react-native-tts module. I added this code to the end of the node_modules/react-native-tts/android/src/main/java/net/no_mad/tts/TextToSpeechModule.java file.

⚠ Please note that this code may be removed if the library is updated or if you reinstall the module using npm install.

After adding the code, I rebuilt the project, and upon checking again, I confirmed that the warning had disappeared.

Tts.removeEventListener Error

After addressing the warning issue, I encountered an error when writing and testing the following code:

useEffect(() => {
    const initTts = async () => {
      // Omitted
    };

    const eventStart = (event) => {
      console.log('start', event);
    }

    const eventFinish = (event) => {
      console.log('finish', event);
    }

    const eventCancel = (event) => {
      console.log('cancel', event);
    }

    Tts.addEventListener("tts-start", eventStart);
    Tts.addEventListener("tts-finish", eventFinish);
    Tts.addEventListener("tts-cancel", eventCancel);

    Tts.getInitStatus().then(initTts);

    return () => {
      Tts.removeEventListener("tts-start", eventStart);
      Tts.removeEventListener("tts-finish", eventFinish);
      Tts.removeEventListener("tts-cancel", eventCancel);
    };
  }, []);

Upon leaving the screen and calling Tts.removeEventListener, the following error occurred:

ERROR  Warning: Internal React error: Attempted to capture a commit phase error inside a detached tree. This indicates a bug in React. Likely causes include deleting the same fiber more than once, committing an already-finishe
d tree, or an inconsistent return pointer.

Error message:

TypeError: undefined is not a function
    in Unknown (created by ...)

Due to an error in Tts.removeEventListener, previous events are not being removed. Consequently, when returning to a screen where Tts is called, events accumulate, resulting in duplicate events triggered by addEventListener.

The following log illustrates this issue when entering the Tts screen for the fourth time and calling Tts.speak only once:

 LOG  start {"utteranceId": "-42970200"}
 LOG  start {"utteranceId": "-42970200"}
 LOG  start {"utteranceId": "-42970200"}
 LOG  start {"utteranceId": "-42970200"}
 LOG  finish {"utteranceId": "-42970200"}
 LOG  finish {"utteranceId": "-42970200"}
 LOG  finish {"utteranceId": "-42970200"}
 LOG  finish {"utteranceId": "-42970200"}

Identifying the Cause

I found information stating that in the latest version of React Native, you cannot use removeEventListener.

removeEventListener was removed in a recent react-native release. You're supposed to call .remove() on the subscription returned by addEventListener now.

Solution

Instead of using Tts.removeEventListener, I modified the code to call .remove() on the subscriptions returned by Tts.addEventListener.

  useEffect(() => {
    const initTts = async () => {
      // Omitted
    };

    const eventStart = (event) => {
      console.log('start', event);
    }

    const eventFinish = (event) => {
      console.log('finish', event);
    }

    const eventCancel = (event) => {
      console.log('cancel', event);
    }

    // Receive subscriptions
    const subscriptionStart = Tts.addEventListener("tts-start", eventStart);
    const subscriptionFinish = Tts.addEventListener("tts-finish", eventFinish);
    const subscriptionCancel = Tts.addEventListener("tts-cancel", eventCancel);
    Tts.getInitStatus().then(initTts);

    return () => {
      // Call .remove() here
      subscriptionStart.remove();
      subscriptionFinish.remove();
      subscriptionCancel.remove();
    };
  }, []);

I can confirm that the error has been resolved, and the events are properly removed when leaving the screen.

TypeScript Issue

After making the change to .remove(), there are no issues with functionality. However, TypeScript shows a red underline and displays the error TS2339: Property 'remove' does not exist on type 'void'.

I find this error bothersome and would like to resolve it as well.

Identifying the Cause

Upon examining the module, I found that the actual return type of addEventListener is EmitterSubscription, but in the index.d.ts file, it is defined as void.

// node_modules/react-native-tts/index.js
// Actual implementation
  addEventListener(type, handler) {
    return this.addListener(type, handler);
  }
// node_modules/react-native-tts/index.d.ts
addEventListener: <T extends TtsEvents>(
    type: T,
    handler: TtsEventHandler<T>
  ) => void; // <-- This is the problematic part

Solution

To resolve this issue, I opted to override the index.d.ts file in the current project without directly modifying the module's index.d.ts file.

I created a react-native-tts folder at the top level of the project and added an index.d.ts file with the following content (only changing the return type of addEventListener to EmitterSubscription):

// react-native-tts/index.d.ts
declare module "react-native-tts" {

  export class ReactNativeTts extends RN.NativeEventEmitter {
    getInitStatus: () => Promise<"success">;
    requestInstallEngine: () => Promise<"success">;
    requestInstallData: () => Promise<"success">;
    setDucking: (enabled: boolean) => Promise<"success">;
    setDefaultEngine: (engineName: string) => Promise<boolean>;
    setDefaultVoice: (voiceId: string) => Promise<"success">;
    setDefaultRate: (rate: number, skipTransform?: boolean) => Promise<"success">;
    setDefaultPitch: (pitch: number) => Promise<"success">;
    setDefaultLanguage: (language: string) => Promise<"success">;
    setIgnoreSilentSwitch: (ignoreSilentSwitch: boolean) => Promise<boolean>;
    voices: () => Promise<Voice[]>;
    engines: () => Promise<Engine[]>;
    /** Read the sentence and return an id for the task. */
    speak: (utterance: string, options?: Options) => string | number;
    stop: (onWordBoundary?: boolean) => Promise<boolean>;
    pause: (onWordBoundary?: boolean) => Promise<boolean>;
    resume: () => Promise<boolean>;
    addEventListener: <T extends TtsEvents>(
      type: T,
      handler: TtsEventHandler<T>
    ) => EmitterSubscription; // <-- Only this line changed
    removeEventListener: <T extends TtsEvents>(
      type: T,
      handler: TtsEventHandler<T>
    ) => void;
  }

  declare const Tts: ReactNativeTts;

  export default Tts;
}

Additionally, for IntelliJ users, if the red underlines persist, you can try clearing the cache by going to File > Invalidate Caches > Invalidate and Restart with all options checked. This should remove the red underlines.

Conclusion

  • Added addListener and removeListener to the 'node_modules/react-native-tts/android/src/main/java/net/no_mad/tts/TextToSpeechModule.java' file.
  • Modified to use the .remove() method instead of removeEventListener.
  • Override the '/react-native-tts/index.d.ts' file to return the EmitterSubscription type.

Now, all yellow warnings, red errors, and red underlines have been resolved.