In a React Native iOS Turbo Module, how should UIKit work reach the main thread, and what do requiresMainQueueSetup and methodQueue change?
answer
- async calls arrive off the main thread
- dispatch_async to the main queue
- methodQueue is marked deprecated
- custom init implies main-queue setup
- unstableRequiresMainQueueSetup runs before JS
basics
~20 sAsynchronous Turbo Module methods arrive on a background method queue, so UIKit calls must be dispatched to the main queue inside the method. +requiresMainQueueSetup only decides where the module is constructed; overriding methodQueue to force every call onto main is deprecated.
solid answer
~40 sUnless a module supplies its own queue, React Native runs its `void` and `Promise` methods on a serial background queue shared with other such modules, and value-returning methods on the JavaScript thread. UIKit is main-thread-only, so wrap just the UIKit part in `dispatch_async(dispatch_get_main_queue(), ^{ ... })` (or `RCTExecuteOnMainQueue`) and settle the promise from there. Overriding `methodQueue` to return the main queue still works, but `RCTBridgeModule.h` marks it deprecated and says to dispatch explicitly. `+requiresMainQueueSetup` returning `YES` makes React Native construct the module on the main thread; return `NO` when `init` does not touch UIKit, because without the method a custom `init` makes React Native assume main-queue setup. The Codegen key `unstableRequiresMainQueueSetup` goes further and initializes the module on main before any JavaScript runs.
code
objective-cpp · 37 lines#import <React/RCTUtils.h> // RCTPresentedViewController
@implementation RCTSmartLock {
SmartLockAdapter *_adapter;
}
// init only builds a Swift object; it needs no UIKit
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (instancetype)init
{
if (self = [super init]) {
_adapter = [SmartLockAdapter new];
}
return self;
}
// pairLock(lockId: string): Promise<void>
- (void)pairLock:(NSString *)lockId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
dispatch_async(dispatch_get_main_queue(), ^{
UIViewController *presenter = RCTPresentedViewController();
if (presenter == nil) {
reject(@"E_NO_PRESENTER", @"No view controller to present from", nil);
return;
}
[self->_adapter presentPairingFrom:presenter lockId:lockId];
resolve(nil);
});
}
@endgo deeper
Remember that native module methods do not run on the main thread by default, so UIKit calls need a dispatch to the main queue.
Explain the default thread for async and sync methods, how dispatch_async to main fits inside a Promise method, and what requiresMainQueueSetup does to construction.
Diagnose main-thread-checker warnings, launch regressions from implicit main-queue setup, and hitches from a main methodQueue, and fix each with the current per-call pattern.
Set a team rule for native modules: no main methodQueue, explicit NO for requiresMainQueueSetup, and unstableRequiresMainQueueSetup only with a measured startup budget.
## Where a Turbo Module's code runs On iOS, React Native's Turbo Module manager decides which thread executes each call. Three kinds of code matter: | Code | Default thread | Source of the rule | |---|---|---| | `void` and `Promise` spec methods | The module's **method queue**; if the module supplies none, a **serial queue shared** with other such modules | Asynchronous calls are dispatched there | | Value-returning spec methods | The **JavaScript thread**, synchronously | JavaScript waits for the return value | | Construction (`init`) | The thread that first requests the module, or the **main thread** when main-queue setup is required | `+requiresMainQueueSetup` and its inference | None of the defaults is the main thread for method calls. **UIKit**, however, must be used from the main thread — presenting the smart-lock vendor's pairing sheet, reading `UIApplication` state, or touching a `UIView` from a background queue is a bug even when it appears to work. ## Getting UIKit work onto main, per call The pattern React Native itself now recommends is **explicit dispatch** around exactly the code that needs it: 1. Do non-UI work — argument validation, SDK calls — on the queue the call arrived on. 2. Wrap only the UIKit part in `dispatch_async(dispatch_get_main_queue(), ^{ ... })`. React Native's `RCTExecuteOnMainQueue` does the same but runs the block immediately if you are already on main. 3. Call `resolve` or `reject` from inside that block once the UI work has finished, so JavaScript observes the result after the sheet is on screen. 4. Keep the block short; long work on main drops frames. For presenting UI, React Native's `RCTPresentedViewController()` helper returns the view controller currently on top, which is the usual anchor for a vendor's sheet; like any UIKit read, call it inside the main-queue block. Never do the reverse from a **synchronous** method: calling back onto main with `dispatch_sync` while the JavaScript thread waits stalls JavaScript on the UI thread and risks deadlock. React Native's own synchronous helper is named `RCTUnsafeExecuteOnMainQueueSync`, and its header asks you not to use it unless you know what you are doing. ## methodQueue: the old lever Legacy modules could override `- (dispatch_queue_t)methodQueue` to return `dispatch_get_main_queue()`, which moved **every** asynchronous method onto main. The Turbo Module manager still honours an overridden queue for backward compatibility, but in React Native 0.87 `RCTBridgeModule.h` marks the property `RCT_DEPRECATED` and gives two replacements: - **for a private work queue**, create your own with `dispatch_queue_create` and dispatch to it; - **for main-thread work**, dispatch to the main queue directly where you need it. Forcing all methods onto main also serializes harmless work behind UI work, so the per-call pattern is faster as well as current. ## requiresMainQueueSetup: where the module is born `+ (BOOL)requiresMainQueueSetup` is about **construction**, not about method calls: - **`YES`** — React Native constructs the module on the main thread, making the requesting thread wait for it. Use it only when `init` genuinely captures UIKit state. - **`NO`** — the module may be constructed on any thread, which is the cheap and usual answer. - **Not implemented** — React Native's backward-compatibility logic infers `YES` if the class overrides `init` or implements `constantsToExport`. A module whose `init` merely creates a Swift adapter therefore pays for main-queue setup until it returns `NO` explicitly. The Codegen configuration offers a stronger variant, `codegenConfig.ios.modules.<Name>.unstableRequiresMainQueueSetup`, documented as initializing the module **on the UI thread before any JavaScript runs**. React Native builds those modules eagerly at startup and the JavaScript bundle waits for them, so every entry costs launch time; the `unstable` prefix is a warning to use it sparingly. ## A diagnostic checklist - **Main-thread checker warnings in Xcode** from inside a module method — a UIKit call on the method queue; wrap it in a main-queue dispatch. - **Launch time grew after adding a module** — look for an implicit `YES` from a custom `init`, or an `unstableRequiresMainQueueSetup` entry. - **The whole app hitches when a method runs** — a `methodQueue` returning main, or long work inside a main-queue block. - **A synchronous getter hangs** — it is waiting on main while main waits on JavaScript; make the method asynchronous. - **A JavaScript `await` never returns after a UI error** — a branch inside the main-queue block returned without calling `resolve` or `reject`.
- Why does a module with a custom init get constructed on the main thread even though it never asked to?When `+requiresMainQueueSetup` is not implemented, React Native's backward-compatibility logic assumes a class that overrides `init` or implements `constantsToExport` might touch UIKit, and sets it up on main. Implementing `+requiresMainQueueSetup` returning `NO` removes that cost when the initializer is UIKit-free.
- When is unstableRequiresMainQueueSetup justified over +requiresMainQueueSetup?Only when the module must capture main-thread state before the first line of JavaScript runs, because it is built eagerly at startup and the bundle waits for it. For a module first used after launch, lazy construction via `+requiresMainQueueSetup` — or no main-queue setup at all — keeps startup cheaper.
saying these in an interview costs you the question
- Turbo Module methods already run on the main thread, so UIKit calls are safe
- requiresMainQueueSetup YES makes every method call run on the main queue
- Overriding methodQueue to return the main queue is the current recommended pattern
- Omitting requiresMainQueueSetup means the module is always set up off the main thread
- dispatch_sync to main from a synchronous method is a safe way to read UIKit state