Skip to main content

How to Get Live Reload Working in a React Native Keyboard Extension

· 3 min read
Rejaul Islam
Creator of react-native-keyboard-bridge

Before this, changing a single line in KeyboardApp.tsx meant a full rebundle and reinstall on both platforms — no Fast Refresh, no live reload, just the standard edit-rebuild-reinstall loop every time. It turns out the fix required two completely different investigations, because Android and iOS started from opposite places.

Android already had this — just never wired up​

CustomKeyboardService.kt already passes useDevSupport = BuildConfig.DEBUG and allowPackagerServerAccess = true to ReactHostImpl. Reading ReactHostImpl's own source confirmed something surprising: whenever both flags are true, it already pings the packager on every surface creation and transparently swaps in a Metro-fetched bundle instead of the committed asset one. This is core upstream React Native behavior — nothing the library needed to add. It silently wasn't working before only because the dev workflow never had Metro running with adb reverse tcp:8081 tcp:8081 set up for the app.

Zero code changes. Just: run npx react-native start, run adb reverse tcp:8081 tcp:8081 (or use react-native run-android, which does it for you), install a debug build, and real Fast Refresh on save just works.

iOS needed an actual code change​

RNKeyboardBootstrap.swift's bundleURL() hardcoded the committed main.jsbundle unconditionally — even in DEBUG builds — because RCTReactNativeFactory has no automatic "check if Metro is up" fallback the way Android's ReactHostImpl does. The fix mirrors the standard RN iOS app template exactly:

override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
Bundle(for: KeyboardReactNativeDelegate.self).url(forResource: "main", withExtension: "jsbundle")
#endif
}

RELEASE builds are completely unaffected — they still load the committed bundle, unchanged.

The one new requirement: temporary network access​

A DEBUG keyboard extension reaching Metro needs network access, which extensions don't have by default. Two one-time, reversible toggles:

  1. Set NSExtension > NSExtensionAttributes > RequestsOpenAccess to true in the extension's Info.plist.
  2. Enable "Allow Full Access" for the keyboard in Settings, once it's installed.

Both should flip back to false/off before a release build — RequestsOpenAccess: true is a real, user-visible permission request with no reason to ship if your keyboard has no other need for network access.

If Metro isn't running when a DEBUG build launches, there's no fallback to the committed bundle — same as any standard debug React Native app. The one difference: an extension process has no UIWindow to present a red error screen in, so the failure can be silent from the UI's perspective. Check Console.app logs for "Could not connect to development server" if the keyboard doesn't render.

Try it yourself​

This is a real, working library, not a proof-of-concept — install it and build your own keyboard:

yarn add react-native-keyboard-bridge