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. ๐Ÿš€

Robot Design Hub Launches

I’ve been working on this search engine site for robot designs for a couple months. It’s now deployed and I will soon launch it on ProductHunt. I used Coolify and Linode for this Python Flask app.

https://robots.greenrobot.com

I would love to know your comments on my design and site.