npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-voice-hold

v1.0.7

Published

React Native Voice library with enhanced hold recording functionality and React Native 0.80+ compatibility fixes

Readme

🎙️ react-native-voice-hold

npm version npm license

Enhanced React Native Voice Recognition Library with Hold Recording Support

A robust React Native voice recognition library with critical bug fixes for React Native 0.76+ and the New Architecture. This package resolves the major event listener issues that break speech-to-text functionality in the latest React Native versions.

🚨 Key Fixes & Improvements

React Native 0.76+ Compatibility

  • Fixed: Critical event listener bug in React Native 0.76+ with New Architecture
  • Solution: Implemented DeviceEventEmitter pattern for reliable event handling
  • Result: Speech-to-text results now work properly in RN 0.76+ and 0.80+

🔥 Critical Fix: Empty Final Results

  • Issue: Android Speech Recognition API sometimes returns empty final results even when partial results contain valid transcription
  • Symptoms: onSpeechResults callback receives empty array [] despite successful partial results
  • Root Cause: Device-specific speech recognition quirks, short speech segments, background noise, timing issues
  • Solution: Implemented comprehensive fallback mechanism using partial results
    • Timeout Fallback: 1-second timeout ensures transcript processing even if final results fail
    • Partial Result Storage: Stores meaningful partial results for fallback use
    • Smart Fallback Logic: Uses partial results when final results are empty
    • Error Recovery: Proper cleanup and interruption handling

🎯 Enhanced Hold Recording

  • New: startHoldRecording() and stopHoldRecording() methods
  • Feature: Continuous recording without auto-stop timeouts
  • Use Case: Perfect for voice messages and long-form dictation

🔧 Production Ready

  • Architecture: Full New Architecture (TurboModules) support
  • Platforms: iOS 11+ and Android API 21+
  • Stability: Comprehensive error handling and state management

📱 Installation

npm install react-native-voice-hold

iOS Setup (Required)

cd ios && pod install

Android Setup

Add microphone permissions to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />

🚀 Quick Start

Basic Usage (Fixed for RN 0.76+)

import Voice from 'react-native-voice-hold';
import { DeviceEventEmitter } from 'react-native';

const VoiceComponent = () => {
  const [results, setResults] = useState<string[]>([]);
  const [isListening, setIsListening] = useState(false);

  useEffect(() => {
    // ✅ CRITICAL: Use DeviceEventEmitter for RN 0.76+ compatibility
    const onSpeechResults = DeviceEventEmitter.addListener(
      'onSpeechResults',
      (e: any) => {
        console.log('📝 Speech results:', e.value);
        setResults(e.value);
      }
    );

    const onSpeechStart = DeviceEventEmitter.addListener(
      'onSpeechStart',
      () => {
        console.log('🎤 Speech started');
        setIsListening(true);
      }
    );

    const onSpeechEnd = DeviceEventEmitter.addListener(
      'onSpeechEnd',
      () => {
        console.log('🔇 Speech ended');
        setIsListening(false);
      }
    );

    return () => {
      onSpeechResults.remove();
      onSpeechStart.remove();
      onSpeechEnd.remove();
    };
  }, []);

  const startListening = async () => {
    try {
      await Voice.start('en-US');
    } catch (error) {
      console.error('❌ Error starting voice recognition:', error);
    }
  };

  const stopListening = async () => {
    try {
      await Voice.stop();
    } catch (error) {
      console.error('❌ Error stopping voice recognition:', error);
    }
  };

  return (
    <View>
      <Button
        title={isListening ? 'Stop Listening' : 'Start Listening'}
        onPress={isListening ? stopListening : startListening}
      />
      {results.map((result, index) => (
        <Text key={index}>{result}</Text>
      ))}
    </View>
  );
};

Hold Recording (New Feature)

import Voice from 'react-native-voice-hold';

// Start continuous recording (perfect for voice messages)
const startHoldRecording = async () => {
  try {
    await Voice.startHoldRecording('en-US', {
      // Hold mode disables silence timeouts
      continuous: true,
      maximumWaitTime: 300000, // 5 minutes max
    });
    console.log('🎙️ Hold recording started');
  } catch (error) {
    console.error('❌ Error starting hold recording:', error);
  }
};

// Stop hold recording manually
const stopHoldRecording = async () => {
  try {
    await Voice.stopHoldRecording();
    console.log('⏹️ Hold recording stopped');
  } catch (error) {
    console.error('❌ Error stopping hold recording:', error);
  }
};

🔥 Critical Fix: Empty Final Results Solution

Hook Implementation (Recommended)

import { useVoiceRecognition } from 'react-native-voice-hold';

const VoiceAssistant = () => {
  const [state, actions] = useVoiceRecognition({
    locale: 'en-US',
    onStart: () => {
      console.log('🎤 Speech started');
      // Clear any existing timeout and reset state
    },
    onEnd: () => {
      console.log('🔇 Speech ended - processing...');
      // 🔥 CRITICAL FIX: 1-second timeout ensures transcript processing
      // even if final results don't come
    },
    onResults: results => {
      console.log('📝 Final results received:', results);
      if (results.length > 0) {
        // Process final results normally
        processTranscript(results[0]);
      } else {
        // 🔥 CRITICAL FIX: Use partial results as fallback
        console.log('⚠️ Final results empty, using partial results fallback');
        // Hook automatically handles fallback logic
      }
    },
    onPartialResults: partial => {
      console.log('🔄 Partial results:', partial);
      // 🔥 CRITICAL FIX: Hook stores partial results for fallback
    },
  });

  // The hook automatically handles all fallback logic!
  return (
    <View>
      <TouchableOpacity
        onPressIn={() => actions.startHoldRecording()}
        onPressOut={() => actions.stopListening()}
      >
        <Text>Hold to Speak</Text>
      </TouchableOpacity>
    </View>
  );
};

Direct Library Implementation

import React, { useEffect, useState, useRef } from 'react';
import { DeviceEventEmitter, Platform } from 'react-native';
import Voice from 'react-native-voice-hold';

const VoiceComponent = () => {
  const [results, setResults] = useState<string[]>([]);
  const [isListening, setIsListening] = useState(false);
  
  // 🔥 CRITICAL FIX: Add refs for fallback mechanism
  const lastPartialResult = useRef<string>('');
  const resultsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const isInterrupted = useRef<boolean>(false);

  useEffect(() => {
    const onSpeechStart = DeviceEventEmitter.addListener('onSpeechStart', () => {
      console.log('🎤 Speech started');
      setIsListening(true);
      // Clear any existing timeout
      if (resultsTimeoutRef.current) {
        clearTimeout(resultsTimeoutRef.current);
        resultsTimeoutRef.current = null;
      }
      lastPartialResult.current = '';
    });

    const onSpeechEnd = DeviceEventEmitter.addListener('onSpeechEnd', () => {
      console.log('🔇 Speech ended - processing...');
      // 🔥 CRITICAL FIX: Add timeout to ensure we process transcript
      resultsTimeoutRef.current = setTimeout(() => {
        console.log('⏰ Timeout: No final results received, using partial results');
        const fallbackTranscript = lastPartialResult.current;
        if (fallbackTranscript.trim() && !isInterrupted.current) {
          console.log('🔄 Timeout fallback transcript:', fallbackTranscript);
          handleTranscript(fallbackTranscript);
        }
      }, 1000); // Wait 1 second for final results
    });

    const onSpeechResults = DeviceEventEmitter.addListener('onSpeechResults', (e) => {
      console.log('📝 Speech results:', e.value);
      // Clear the timeout since we got results
      if (resultsTimeoutRef.current) {
        clearTimeout(resultsTimeoutRef.current);
        resultsTimeoutRef.current = null;
      }

      if (e.value && e.value.length > 0) {
        const finalTranscript = e.value[0];
        console.log('⚡ Fast transcript:', finalTranscript);
        handleTranscript(finalTranscript);
      } else {
        // 🔥 CRITICAL FIX: Use partial results as fallback
        console.log('⚠️ Final results empty, checking partial results...');
        const fallbackTranscript = lastPartialResult.current;
        if (fallbackTranscript.trim()) {
          console.log('🔄 Using fallback transcript:', fallbackTranscript);
          handleTranscript(fallbackTranscript);
        }
      }
    });

    const onSpeechPartialResults = DeviceEventEmitter.addListener('onSpeechPartialResults', (e) => {
      console.log('🔄 Partial results:', e.value);
      if (e.value && e.value.length > 0) {
        const partialTranscript = e.value[0];
        // 🔥 CRITICAL FIX: Store the last partial result for fallback
        if (partialTranscript.trim()) {
          lastPartialResult.current = partialTranscript;
        }
      }
    });

    return () => {
      onSpeechStart.remove();
      onSpeechEnd.remove();
      onSpeechResults.remove();
      onSpeechPartialResults.remove();
      // Clear timeout on cleanup
      if (resultsTimeoutRef.current) {
        clearTimeout(resultsTimeoutRef.current);
        resultsTimeoutRef.current = null;
      }
      Voice.destroy();
    };
  }, []);

  const handleTranscript = (transcript: string) => {
    console.log('🎯 Processing transcript:', transcript);
    setResults([transcript]);
  };

  // ... rest of component
};

📖 Complete API Reference

Standard Methods

  • Voice.start(locale) - Start voice recognition
  • Voice.stop() - Stop voice recognition
  • Voice.cancel() - Cancel voice recognition
  • Voice.destroy() - Clean up resources
  • Voice.isAvailable() - Check if voice recognition is available
  • Voice.isRecognizing() - Check if currently recognizing

Hold Recording Methods (New)

  • Voice.startHoldRecording(locale, options) - Start continuous recording
  • Voice.stopHoldRecording() - Stop hold recording

Event Listeners (DeviceEventEmitter - RN 0.76+ Compatible)

// ✅ CORRECT: Use DeviceEventEmitter
DeviceEventEmitter.addListener('onSpeechStart', handler);
DeviceEventEmitter.addListener('onSpeechRecognized', handler);
DeviceEventEmitter.addListener('onSpeechEnd', handler);
DeviceEventEmitter.addListener('onSpeechError', handler);
DeviceEventEmitter.addListener('onSpeechResults', handler);
DeviceEventEmitter.addListener('onSpeechPartialResults', handler);
DeviceEventEmitter.addListener('onSpeechVolumeChanged', handler);

// ❌ BROKEN: Direct assignment (doesn't work in RN 0.76+)
Voice.onSpeechResults = handler; // DON'T USE

🔄 Migration from react-native-voice

  1. Install the new package:

    npm uninstall @react-native-voice/voice
    npm install react-native-voice-hold
  2. Update your imports:

    // Old

import Voice from '@react-native-voice/voice';

// New import Voice from 'react-native-voice-hold';


3. **Update event listeners for RN 0.76+ compatibility:**
```typescript
// Old (broken in RN 0.76+)
Voice.onSpeechResults = (e) => console.log(e.value);

// New (works in all RN versions)
DeviceEventEmitter.addListener('onSpeechResults', (e) => console.log(e.value));
  1. Add fallback logic for empty final results:
    // The useVoiceRecognition hook automatically handles this!
    // Or implement the fallback pattern shown above for direct usage

🐛 Troubleshooting

Empty Final Results

Problem: onSpeechResults receives empty array despite successful partial results.

Solution: ✅ Fixed in this package - The hook and examples above include comprehensive fallback logic.

Event Listeners Not Working (RN 0.76+)

Problem: Speech events not firing in React Native 0.76+.

Solution: ✅ Fixed - Use DeviceEventEmitter pattern instead of direct assignment.

Hold Recording Not Working

Problem: Hold recording stops unexpectedly.

Solution: ✅ Fixed - Proper hold mode state management prevents premature stopping.

📄 License

MIT License - see LICENSE file for details.

🤝 Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests.


Built with ❤️ for the React Native community