diff --git a/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm b/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm index b1d1b16b45ee..6702584d1af3 100644 --- a/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm +++ b/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import #import #import @@ -241,4 +242,30 @@ - (void)testBundleLoadAwaitsMainQueueModuleSetup @"FakeEagerModule -init must have fully returned before the JS bundle finishes loading"); } +- (void)testInvalidateDoesNotDeadlockWhenSuspendCallsIntoJS +{ + OCMStub([_mockDelegate unstableModulesRequiringMainQueueSetup]).andReturn(@[]); + + RCTInstance *instance = [self makeInstance]; + __weak RCTInstance *weakInstance = instance; + + // Stands in for a surface observer that calls into JS while suspend stops surfaces. + id presenterMock = OCMPartialMock(instance.surfacePresenter); + OCMStub([(RCTSurfacePresenter *)presenterMock suspend]).andDo(^(NSInvocation *invocation) { + [weakInstance callFunctionOnJSModule:@"RCTLog" method:@"logIfNoNativeHook" args:@[]]; + BOOL result = YES; + [invocation setReturnValue:&result]; + }); + + // Invalidate off-main so a deadlock fails the wait instead of hanging the test runner. + XCTestExpectation *invalidated = [self expectationWithDescription:@"invalidate returned"]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + [instance invalidate]; + [invalidated fulfill]; + }); + [self waitForExpectations:@[ invalidated ] timeout:5.0]; + + [presenterMock stopMocking]; +} + @end diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index f4de8848613a..5efe16f73de6 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -183,11 +183,15 @@ - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method - (void)invalidate { - std::lock_guard lock(_invalidationMutex); - _valid = false; - if (_reactInstance) { - _reactInstance->unregisterFromInspector(); + { + std::lock_guard lock(_invalidationMutex); + _valid = false; + if (_reactInstance) { + _reactInstance->unregisterFromInspector(); + } } + // suspend shouldn't be called under _invalidationMutex: it notifies surface observers, + // which can call callFunctionOnJSModule and deadlock on the same mutex. [_surfacePresenter suspend]; [_jsThreadManager dispatchToJSThread:^{ /**