Handling Key Events in React Native macOS: The Native Approach

After struggling with unreliable keyboard event handling in React Native macOS, I discovered the solution: handle key events at the native macOS level instead of trying to make JavaScript event handling work reliably.

The Problem

Standard React Native keyboard handling approaches often fail on macOS:

  • onKeyDown props don’t capture events consistently
  • document.addEventListener doesn’t exist in React Native
  • Focus management interferes with text editing
  • Chirping sounds when events aren’t properly consumed
  • Events get lost when focus changes between UI elements

The Solution: Native + React Native Bridge

The key insight is to handle keyboard events at the native macOS level using Objective-C, then bridge them to React Native when needed.

Step 1: Native Keyboard Handling (AppDelegate.mm)

Add native keyboard monitoring to your AppDelegate.mm:

#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTBridge.h>

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
  self.moduleName = @"YourAppName";
  self.initialProps = @{};
  [super applicationDidFinishLaunching:notification];
  
  // Set up native keyboard handling
  [self setupNativeKeyboardHandling];
}

- (void)setupNativeKeyboardHandling
{
  // Add local monitor for key events (equivalent to SwiftUI's onKeyPress)
  [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
    NSLog(@"🔑 NATIVE: Key event - keyCode: %d, characters: %@", (int)event.keyCode, event.characters);
    
    // Handle Escape key (keyCode 53)
    if (event.keyCode == 53) {
      NSLog(@"🔑 NATIVE: Escape key detected, sending to React Native");
      [self sendEscapeKeyToReactNative];
      
      // Return nil to consume the event (equivalent to SwiftUI's .handled)
      return nil;
    }
    
    // Return the event for other keys to continue normal processing
    return event;
  }];
  
  NSLog(@"🔑 NATIVE: Native keyboard handling setup complete");
}

- (void)sendEscapeKeyToReactNative
{
  // Send event to React Native via DeviceEventEmitter
  [[NSNotificationCenter defaultCenter] postNotificationName:@"NativeEscapeKeyPressed" object:nil];
  
  if (self.bridge) {
    [self.bridge.eventDispatcher sendAppEventWithName:@"NativeEscapeKeyPressed" body:@{}];
  }
}

@end

Step 2: React Native Event Listener (App.tsx)

Listen for native events in your React Native app:

import React from 'react';
import { Platform, View, DeviceEventEmitter } from 'react-native';

const GlobalKeyboardWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isVisible, closePanel } = useTaskDetailPanel();

  // Listen for native keyboard events from AppDelegate
  React.useEffect(() => {
    if (Platform.OS !== 'macos') return;

    console.log('🔑 NATIVE: Setting up native keyboard event listener');

    // Listen for native escape key events
    const subscription = DeviceEventEmitter.addListener('NativeEscapeKeyPressed', () => {
      console.log('🔑 NATIVE: Escape key received from native AppDelegate');
      if (isVisible) {
        console.log('🔑 NATIVE: Closing TaskDetailPanel via native event');
        closePanel();
      }
    });

    return () => {
      subscription.remove();
      console.log('🔑 NATIVE: Native keyboard event listener removed');
    };
  }, [isVisible, closePanel]);

  return (
    <View style={{ flex: 1 }}>
      {children}
    </View>
  );
};

// Wrap your app with the keyboard wrapper
function App() {
  return (
    <GlobalKeyboardWrapper>
      {/* Your app content */}
    </GlobalKeyboardWrapper>
  );
}

Key Benefits

  1. Reliable Event Capture: Native macOS event monitoring captures ALL key events
  2. No Chirping: Events are consumed at the native level (return nil)
  3. No Focus Issues: Doesn’t interfere with text editing or UI focus
  4. Performance: Native event handling is faster than JavaScript
  5. Extensible: Easy to add more key combinations

Key Codes Reference

Common macOS key codes for event.keyCode:

  • Escape: 53
  • Enter: 36
  • Space: 49
  • Arrow Up: 126
  • Arrow Down: 125
  • Arrow Left: 123
  • Arrow Right: 124

Why This Works

  1. Native Level: NSEvent addLocalMonitorForEventsMatchingMask captures events before they reach React Native
  2. Event Consumption: Returning nil consumes the event (like SwiftUI’s .handled)
  3. Selective Bridging: Only send events to React Native when needed
  4. Clean Separation: Native handles the mechanics, React Native handles the logic

This approach finally gave me reliable global keyboard shortcuts in React Native macOS with zero interference and zero chirping sounds! 🎉


Building cross-platform apps with React Native macOS. Sometimes you need to go native to get it right. 🚀

Leave a Comment