tezvyn:

Native Module Threading Model

AI-drafted, machine-checkedintermediate

Native modules run on their own threads, not the JS thread or main UI thread. Heavy work stays off the JS event loop, but UI mutations must be manually dispatched to the main thread. Assuming they are main-thread safe causes crashes and ANRs.

WHY IT EXISTS: JavaScript in React Native runs on a single dedicated thread that must never block. If native code executed synchronously on that same thread, every database query, camera capture, or Bluetooth scan would freeze the UI and break the 60 FPS contract. The threading model exists to keep the JS event loop unblocked while letting platform code do heavy lifting on its own schedulers. It also exists to respect platform rules that require UI mutations to happen only on the main thread, preventing corruption in the view hierarchy.

THE MENTAL MODEL: Picture two translators in separate soundproof booths. The JS booth sends a request through a mail slot; the native booth picks it up later, does the work, and slides the answer back. They never share the same desk. This means you cannot touch UI state from the native booth unless you explicitly walk over to the main booth, because on both iOS and Android the UI lives in a specific room with a locked door that only opens for the main thread.

HOW IT WORKS: On iOS, the bridge creates a dedicated serial GCD queue for each native module unless you override methodQueue to return a custom one. Your Objective-C or Swift methods run on that queue, not the main thread. On Android, the ReactInstanceManager invokes ReactMethod-annotated functions on a background native module thread by default. To update a View or call a UI-only API, you must dispatch to the main thread using runOnUiThread on Android or dispatch_async to the main queue on iOS. Callbacks and promises always resolve back onto the JS thread automatically, but the native execution begins on a background thread and stays there unless you move it.

WHEN TO USE IT: Use the default background queue for any I/O, image processing, encryption, or long-running computation. Dispatch to the main thread only when you need to synchronously read layout metrics, trigger an imperative animation, or call a platform API that is main-thread-only such as UIKit or Android View methods. If you have a module that manages a shared hardware resource like a serial Bluetooth peripheral, override the queue to a single serial queue to prevent interleaved commands from different threads.

WHEN NOT TO USE IT: Do not run all native module logic on the main thread as a default. That causes ANRs on Android and dropped frames on iOS. Do not assume that because JS called your method, you are running on the JS thread; mutable state shared between JS and native is not automatically thread-safe. Do not perform blocking network calls on the main thread dispatch, and do not hold locks across bridge calls because deadlocks can occur when JS waits for native and native waits for JS.

ONE CANONICAL EXAMPLE: Imagine a native module called CalendarManager with a method fetchEvents that reads the device calendar and a method dismissModal that imperatively closes a React Native modal. The fetchEvents implementation can safely query the calendar database on its default background queue and return a promise. However, dismissModal must call UIViewController dismissal code on iOS or Dialog dismissal on Android from the main thread. If you forget the main-thread dispatch in dismissModal, iOS throws a UIKitThreadAssertionException and Android may deadlock or throw an IllegalStateException. The correct pattern is to keep data work on the background thread and wrap every UI touch in an explicit main-thread dispatch.

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.