From c7dccf5dc03630aa35a0317bae23710fba9c1f12 Mon Sep 17 00:00:00 2001
From: Tom Duncalf <tom.duncalf@mongodb.com>
Date: Thu, 10 Feb 2022 11:51:20 +0000
Subject: [PATCH 1/6] Flush the React Native UI task queue whenever we call
 from C++ to JS by calling jsCallInvoker->invokeAsync.

React Native has its own JS "microtask" queue, implemented in https://github.com/facebook/react-native/blob/main/Libraries/Core/Timers/JSTimers.js. Any asynchronous work, e.g. `setTimeout` or `setImmediate`, is added to this queue, and the queue is flushed whenever a message is sent from React Native's native core to JS: https://github.com/facebook/react-native/blob/main/Libraries/BatchedBridge/MessageQueue.js#L108.

However, our native to JS messages are not being passed via this abstraction - instead, we hook directly into the JS engine. This means that React Native is not aware that when Realm has done some async work, we might need to update the UI (and therefore flush the task queue), and this can result in Realm-related UI updates not showing until some action is taken which causes React Native to send a message from the core to JS (e.g. touching the screen), which flushes this task queue, resolving pending promises and updating the UI.

This commit calls the React Native jsCallInvoker->invokeAsync method, which internally flushes the task queue. As this method is async, we wait for any current pending invocation to complete before triggering another one (using a flag, so the call is "debounced")
---
 react-native/ios/RealmReact/RealmReact.mm | 23 ++++++++++++++++++++---
 src/jsc/jsc_class.hpp                     |  5 ++++-
 src/jsc/jsc_function.hpp                  |  9 +++++++++
 src/jsc/jsc_init.cpp                      |  8 ++++++--
 src/jsc/jsc_init.h                        |  3 ++-
 src/jsc/rpc.cpp                           |  2 +-
 6 files changed, 42 insertions(+), 8 deletions(-)

diff --git a/react-native/ios/RealmReact/RealmReact.mm b/react-native/ios/RealmReact/RealmReact.mm
index 4bcc1dee97..fe898031a6 100644
--- a/react-native/ios/RealmReact/RealmReact.mm
+++ b/react-native/ios/RealmReact/RealmReact.mm
@@ -22,6 +22,7 @@
 
 #import <React/RCTBridge+Private.h>
 #import <React/RCTJavaScriptExecutor.h>
+#include <ReactCommon/CallInvoker.h>
 
 #import <objc/runtime.h>
 #import <arpa/inet.h>
@@ -51,6 +52,7 @@ - (JSContext *)context;
 @interface RCTBridge (Realm_RCTCxxBridge)
 - (JSGlobalContextRef)jsContextRef;
 - (void *)runtime;
+- (std::shared_ptr<facebook::react::CallInvoker>)jsCallInvoker;
 @end
 
 extern "C" JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create) {
@@ -83,6 +85,7 @@ @interface RealmReact () <RCTBridgeModule>
 
 @implementation RealmReact {
     NSMutableDictionary *_eventHandlers;
+    bool waitingForFlush;
 
 #if DEBUG
     GCDWebServer *_webServer;
@@ -108,6 +111,7 @@ - (instancetype)init {
     self = [super init];
     if (self) {
         _eventHandlers = [[NSMutableDictionary alloc] init];
+        waitingForFlush = false;
     }
     return self;
 }
@@ -274,7 +278,7 @@ - (void)dealloc {
 
 typedef JSGlobalContextRef (^JSContextRefExtractor)();
 
-void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor) {
+void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor, std::function<void()> flushUiQueue) {
     // Make sure the previous JS thread is completely finished before continuing.
     static __weak NSThread *s_currentJSThread;
     while (s_currentJSThread && !s_currentJSThread.finished) {
@@ -282,7 +286,7 @@ void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor) {
     }
     s_currentJSThread = [NSThread currentThread];
 
-    RJSInitializeInContext(jsContextExtractor());
+    RJSInitializeInContext(jsContextExtractor(), flushUiQueue);
 }
 
 - (void)setBridge:(RCTBridge *)bridge {
@@ -310,7 +314,7 @@ - (void)setBridge:(RCTBridge *)bridge {
             if (!self || !bridge) {
                 return;
             }
-
+            
             _initializeOnJSThread(^{
                 // RN < 0.58 has a private method that returns the js context
                 if ([bridge respondsToSelector:@selector(jsContextRef)]) {
@@ -325,6 +329,17 @@ - (void)setBridge:(RCTBridge *)bridge {
                     JSGlobalContextRef ctx_;
                 };
                 return static_cast<RealmJSCRuntime*>(bridge.runtime)->ctx_;
+            }, ^{
+                if (!waitingForFlush) {
+                    waitingForFlush = true;
+                    // Calling jsCallInvokver->invokeAsync restuls in React Native flushing any pending UI
+                    // updates after we have called into JS from C++
+                    //
+                    // TODO add a bit more info
+                    [bridge jsCallInvoker]->invokeAsync([&](){
+                        waitingForFlush = false;
+                    });
+                }
             });
         } queue:RCTJSThread];
     } else { // React Native 0.44 and older
@@ -341,6 +356,8 @@ - (void)setBridge:(RCTBridge *)bridge {
 
             _initializeOnJSThread(^ {
                 return RealmReactGetJSGlobalContextForExecutor(executor, true);
+            }, [&]() {
+                // jsCallInvoker does not exist on older RN
             });
         }];
     }
diff --git a/src/jsc/jsc_class.hpp b/src/jsc/jsc_class.hpp
index 984bf55454..8d9d633794 100644
--- a/src/jsc/jsc_class.hpp
+++ b/src/jsc/jsc_class.hpp
@@ -19,6 +19,7 @@
 #pragma once
 
 #include "jsc_types.hpp"
+#include "jsc_function.hpp"
 
 #include "js_class.hpp"
 #include "js_util.hpp"
@@ -41,7 +42,7 @@ extern js::Protected<JSObjectRef> FunctionPrototype;
 extern js::Protected<JSObjectRef> RealmObjectClassConstructor;
 extern js::Protected<JSObjectRef> RealmObjectClassConstructorPrototype;
 
-static inline void jsc_class_init(JSContextRef ctx, JSObjectRef globalObject)
+static inline void jsc_class_init(JSContextRef ctx, JSObjectRef globalObject, std::function<void()> flushUiQueue)
 {
     // handle ReactNative app refresh by reseting the cached constructor values
     if (RealmObjectClassConstructor) {
@@ -63,6 +64,8 @@ static inline void jsc_class_init(JSContextRef ctx, JSObjectRef globalObject)
     JSObjectRef globalFunction = jsc::Value::to_object(ctx, value);
     value = jsc::Object::get_property(ctx, globalFunction, "prototype");
     FunctionPrototype = js::Protected<JSObjectRef>(ctx, Value::to_object(ctx, value));
+
+    js::flush_ui_queue = flushUiQueue;
 }
 
 template <typename T>
diff --git a/src/jsc/jsc_function.hpp b/src/jsc/jsc_function.hpp
index d2f99d5c1e..d920009eac 100644
--- a/src/jsc/jsc_function.hpp
+++ b/src/jsc/jsc_function.hpp
@@ -23,12 +23,18 @@
 namespace realm {
 namespace js {
 
+// TODO
+extern std::function<void()> flush_ui_queue;
+
 template <>
 inline JSValueRef jsc::Function::call(JSContextRef ctx, const JSObjectRef& function, const JSObjectRef& this_object,
                                       size_t argc, const JSValueRef arguments[])
 {
     JSValueRef exception = nullptr;
     JSValueRef result = JSObjectCallAsFunction(ctx, function, this_object, argc, arguments, &exception);
+
+    flush_ui_queue();
+
     if (exception) {
         throw jsc::Exception(ctx, exception);
     }
@@ -48,6 +54,9 @@ inline JSObjectRef jsc::Function::construct(JSContextRef ctx, const JSObjectRef&
 {
     JSValueRef exception = nullptr;
     JSObjectRef result = JSObjectCallAsConstructor(ctx, function, argc, arguments, &exception);
+
+    flush_ui_queue();
+
     if (exception) {
         throw jsc::Exception(ctx, exception);
     }
diff --git a/src/jsc/jsc_init.cpp b/src/jsc/jsc_init.cpp
index ba290616b7..63d3070f98 100644
--- a/src/jsc/jsc_init.cpp
+++ b/src/jsc/jsc_init.cpp
@@ -31,6 +31,10 @@ js::Protected<JSObjectRef> FunctionPrototype;
 js::Protected<JSObjectRef> RealmObjectClassConstructor;
 js::Protected<JSObjectRef> RealmObjectClassConstructorPrototype;
 } // namespace jsc
+
+namespace js {
+std::function<void()> flush_ui_queue;
+} // namespace js
 } // namespace realm
 
 extern "C" {
@@ -43,13 +47,13 @@ JSObjectRef RJSConstructorCreate(JSContextRef ctx)
     return js::RealmClass<Types>::create_constructor(ctx);
 }
 
-void RJSInitializeInContext(JSContextRef ctx)
+void RJSInitializeInContext(JSContextRef ctx, std::function<void()> flush_ui_queue)
 {
     static const jsc::String realm_string = "Realm";
 
     JSObjectRef global_object = JSContextGetGlobalObject(ctx);
 
-    jsc_class_init(ctx, global_object);
+    jsc_class_init(ctx, global_object, flush_ui_queue);
 
     JSObjectRef realm_constructor = RJSConstructorCreate(ctx);
 
diff --git a/src/jsc/jsc_init.h b/src/jsc/jsc_init.h
index 8521abef9d..8d88e4d462 100644
--- a/src/jsc/jsc_init.h
+++ b/src/jsc/jsc_init.h
@@ -19,13 +19,14 @@
 #pragma once
 
 #include <JavaScriptCore/JSBase.h>
+#include <functional>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 JSObjectRef RJSConstructorCreate(JSContextRef ctx);
-void RJSInitializeInContext(JSContextRef ctx);
+void RJSInitializeInContext(JSContextRef ctx, std::function<void()> send_dummy_event);
 void RJSInvalidateCaches();
 
 #ifdef __cplusplus
diff --git a/src/jsc/rpc.cpp b/src/jsc/rpc.cpp
index 9940ea3011..1b963a5d89 100644
--- a/src/jsc/rpc.cpp
+++ b/src/jsc/rpc.cpp
@@ -387,7 +387,7 @@ RPCServerImpl::RPCServerImpl()
     }
 
     m_requests["/create_session"] = [this](const json dict) {
-        RJSInitializeInContext(m_context);
+        RJSInitializeInContext(m_context, []() {});
 
         jsc::String realm_string = "Realm";
         JSObjectRef realm_constructor =

From 3cd2bb45905a6ca141457a6458e10215ad4d4b1b Mon Sep 17 00:00:00 2001
From: Tom Duncalf <tom.duncalf@mongodb.com>
Date: Mon, 25 Apr 2022 10:36:32 +0100
Subject: [PATCH 2/6] WIP

---
 example/ios/Podfile.lock                         |  4 ++--
 .../xcshareddata/IDEWorkspaceChecks.plist        |  8 ++++++++
 react-native/ios/RealmReact/RealmReact.mm        | 16 ++++++++++++----
 3 files changed, 22 insertions(+), 6 deletions(-)
 create mode 100644 example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index b4a4e0f891..3e1a948ded 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -342,7 +342,7 @@ PODS:
     - React-jsi (= 0.66.4)
     - React-logger (= 0.66.4)
     - React-perflogger (= 0.66.4)
-  - RealmJS (10.14.0):
+  - RealmJS (10.16.0):
     - GCDWebServer
     - React
   - Yoga (1.14.0)
@@ -535,7 +535,7 @@ SPEC CHECKSUMS:
   React-RCTVibration: e3ffca672dd3772536cb844274094b0e2c31b187
   React-runtimeexecutor: dec32ee6f2e2a26e13e58152271535fadff5455a
   ReactCommon: 57b69f6383eafcbd7da625bfa6003810332313c4
-  RealmJS: 5a939376f3bdd94708abe2326d33d8f935b9fb37
+  RealmJS: 8a3478957315c29cdc0b3f958f2e370d22330b2d
   Yoga: e7dc4e71caba6472ff48ad7d234389b91dadc280
   YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
 
diff --git a/example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/react-native/ios/RealmReact/RealmReact.mm b/react-native/ios/RealmReact/RealmReact.mm
index fe898031a6..0880c42876 100644
--- a/react-native/ios/RealmReact/RealmReact.mm
+++ b/react-native/ios/RealmReact/RealmReact.mm
@@ -330,14 +330,22 @@ - (void)setBridge:(RCTBridge *)bridge {
                 };
                 return static_cast<RealmJSCRuntime*>(bridge.runtime)->ctx_;
             }, ^{
+                // Calling jsCallInvokver->invokeAsync causes React Native to flush any pending UI
+                // updates. We call this after we have called into JS from C++, in order to ensure 
+                // the UI updates in response to any changes, as we bypass the "normal" RN calling
+                // mechanisms (see #4389, facebook/react-native#33006).
+                //
+                // Calls are debounced using the waitingForFlush flag, so if an async flush is already
+                // pending when another JS to C++ call happens, we don't call invokeAsync again. It 
+                // might be possible to further optimize this, e.g. only call max once per frame.
                 if (!waitingForFlush) {
                     waitingForFlush = true;
-                    // Calling jsCallInvokver->invokeAsync restuls in React Native flushing any pending UI
-                    // updates after we have called into JS from C++
-                    //
-                    // TODO add a bit more info
                     [bridge jsCallInvoker]->invokeAsync([&](){
                         waitingForFlush = false;
+
+                        if (waitingForFlush) {
+                            [bridge jsCallInvoker]->invokeAsync([&](){});
+                        }
                     });
                 }
             });

From b64713833b4a857f75d30ab4d7c9375acdb1e6d0 Mon Sep 17 00:00:00 2001
From: Tom Duncalf <tom.duncalf@mongodb.com>
Date: Tue, 26 Apr 2022 11:31:34 +0100
Subject: [PATCH 3/6] Add no-op UI flush for Android, will be implemented in
 Hermes

---
 src/android/jsc_override.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/android/jsc_override.cpp b/src/android/jsc_override.cpp
index 88a9f6610a..450897ad0f 100644
--- a/src/android/jsc_override.cpp
+++ b/src/android/jsc_override.cpp
@@ -144,7 +144,7 @@ static JSGlobalContextRef create_context(JSContextGroupRef group, JSClassRef glo
     // Clear cache from previous instances.
     RJSInvalidateCaches();
 
-    RJSInitializeInContext(ctx);
+    RJSInitializeInContext(ctx, []() {});
     realmContextInjected = true;
     return ctx;
 }

From 1a1b6e314fb6a9299ba279116a9d9f7a083ba6a1 Mon Sep 17 00:00:00 2001
From: Tom Duncalf <tom.duncalf@mongodb.com>
Date: Tue, 26 Apr 2022 12:04:35 +0100
Subject: [PATCH 4/6] Tidy up PR

---
 .../xcshareddata/IDEWorkspaceChecks.plist     |  8 ----
 react-native/ios/RealmReact/RealmReact.mm     | 39 ++++++++++++-------
 src/android/jsc_override.cpp                  |  3 ++
 src/jsc/jsc_function.hpp                      |  2 +-
 src/jsc/jsc_init.h                            |  2 +-
 5 files changed, 29 insertions(+), 25 deletions(-)
 delete mode 100644 example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

diff --git a/example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d981003d..0000000000
--- a/example/ios/RealmTsTemplate.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>
diff --git a/react-native/ios/RealmReact/RealmReact.mm b/react-native/ios/RealmReact/RealmReact.mm
index 0880c42876..dc620a5233 100644
--- a/react-native/ios/RealmReact/RealmReact.mm
+++ b/react-native/ios/RealmReact/RealmReact.mm
@@ -52,6 +52,7 @@ - (JSContext *)context;
 @interface RCTBridge (Realm_RCTCxxBridge)
 - (JSGlobalContextRef)jsContextRef;
 - (void *)runtime;
+// Expose the CallInvoker so that we can call `invokeAsync`
 - (std::shared_ptr<facebook::react::CallInvoker>)jsCallInvoker;
 @end
 
@@ -85,7 +86,8 @@ @interface RealmReact () <RCTBridgeModule>
 
 @implementation RealmReact {
     NSMutableDictionary *_eventHandlers;
-    bool waitingForFlush;
+    // Keep track of whether we are already waiting for the React Native UI queue to be flushed asynchronously
+    bool waitingForUiFlush;
 
 #if DEBUG
     GCDWebServer *_webServer;
@@ -111,7 +113,7 @@ - (instancetype)init {
     self = [super init];
     if (self) {
         _eventHandlers = [[NSMutableDictionary alloc] init];
-        waitingForFlush = false;
+        waitingForUiFlush = false;
     }
     return self;
 }
@@ -278,7 +280,7 @@ - (void)dealloc {
 
 typedef JSGlobalContextRef (^JSContextRefExtractor)();
 
-void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor, std::function<void()> flushUiQueue) {
+void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor, std::function<void()> flushUiQueueFn) {
     // Make sure the previous JS thread is completely finished before continuing.
     static __weak NSThread *s_currentJSThread;
     while (s_currentJSThread && !s_currentJSThread.finished) {
@@ -286,7 +288,7 @@ void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor, std::functi
     }
     s_currentJSThread = [NSThread currentThread];
 
-    RJSInitializeInContext(jsContextExtractor(), flushUiQueue);
+    RJSInitializeInContext(jsContextExtractor(), flushUiQueueFn);
 }
 
 - (void)setBridge:(RCTBridge *)bridge {
@@ -330,20 +332,27 @@ - (void)setBridge:(RCTBridge *)bridge {
                 };
                 return static_cast<RealmJSCRuntime*>(bridge.runtime)->ctx_;
             }, ^{
-                // Calling jsCallInvokver->invokeAsync causes React Native to flush any pending UI
-                // updates. We call this after we have called into JS from C++, in order to ensure 
-                // the UI updates in response to any changes, as we bypass the "normal" RN calling
-                // mechanisms (see #4389, facebook/react-native#33006).
+                // Calling jsCallInvokver->invokeAsync tells React Native to execute the lambda passed
+                // in on the JS thread, and then flush the internal "microtask queue", which has the 
+                // effect of flushing any pending UI updates. 
                 //
-                // Calls are debounced using the waitingForFlush flag, so if an async flush is already
-                // pending when another JS to C++ call happens, we don't call invokeAsync again. It 
-                // might be possible to further optimize this, e.g. only call max once per frame.
-                if (!waitingForFlush) {
-                    waitingForFlush = true;
+                // We call this after we have called into JS from C++, in order to ensure that the RN
+                // UI updates in response to any changes from Realm. We need to do this as we bypass
+                // the usual RN bridge mechanism for communicating between C++ and JS, so without doing
+                // this RN has no way to know that a change has occurred which might require an update
+                // (see #4389, facebook/react-native#33006).
+                //
+                // Calls are debounced using the waitingForUiFlush flag, so if an async flush is already
+                // pending when another JS to C++ call happens, we don't call invokeAsync again. This works
+                // because the work is performed before the microtask queue is flushed - see sequence
+                // diagram at https://bit.ly/3kexhHm. It might be possible to further optimize this, 
+                // e.g. only flush the queue a maximum of once per frame, but this seems reasonable.
+                if (!waitingForUiFlush) {
+                    waitingForUiFlush = true;
                     [bridge jsCallInvoker]->invokeAsync([&](){
-                        waitingForFlush = false;
+                        waitingForUiFlush = false;
 
-                        if (waitingForFlush) {
+                        if (waitingForUiFlush) {
                             [bridge jsCallInvoker]->invokeAsync([&](){});
                         }
                     });
diff --git a/src/android/jsc_override.cpp b/src/android/jsc_override.cpp
index 450897ad0f..580a42e2bd 100644
--- a/src/android/jsc_override.cpp
+++ b/src/android/jsc_override.cpp
@@ -144,6 +144,9 @@ static JSGlobalContextRef create_context(JSContextGroupRef group, JSClassRef glo
     // Clear cache from previous instances.
     RJSInvalidateCaches();
 
+    // We pass a no-op lambda for the `flushUiQueue` function, as there's no easy way to
+    // access the React Native context from here and the UI flush bug (#4389) has not been
+    // seen on Android. We should be able to do this properly on Hermes.
     RJSInitializeInContext(ctx, []() {});
     realmContextInjected = true;
     return ctx;
diff --git a/src/jsc/jsc_function.hpp b/src/jsc/jsc_function.hpp
index d920009eac..4ce04ef477 100644
--- a/src/jsc/jsc_function.hpp
+++ b/src/jsc/jsc_function.hpp
@@ -23,7 +23,7 @@
 namespace realm {
 namespace js {
 
-// TODO
+// Function passed in from the React Native initialisation code to flush the UI microtask queue
 extern std::function<void()> flush_ui_queue;
 
 template <>
diff --git a/src/jsc/jsc_init.h b/src/jsc/jsc_init.h
index 8d88e4d462..96f7ddb168 100644
--- a/src/jsc/jsc_init.h
+++ b/src/jsc/jsc_init.h
@@ -26,7 +26,7 @@ extern "C" {
 #endif
 
 JSObjectRef RJSConstructorCreate(JSContextRef ctx);
-void RJSInitializeInContext(JSContextRef ctx, std::function<void()> send_dummy_event);
+void RJSInitializeInContext(JSContextRef ctx, std::function<void()> flush_ui_queue);
 void RJSInvalidateCaches();
 
 #ifdef __cplusplus

From e61a58afc8d8664233fa19d647a8d20e47506ed5 Mon Sep 17 00:00:00 2001
From: Tom Duncalf <tom.duncalf@mongodb.com>
Date: Tue, 26 Apr 2022 13:08:31 +0100
Subject: [PATCH 5/6] Add changelog

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5372da7145..914b4c3f7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ x.x.x Release notes (yyyy-MM-dd)
 * None.
 
 ### Fixed
-* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
+* Fixed issue where React Native apps would sometimes show stale Realm data until the user interacted with the app UI ([#4389](https://github.com/realm/realm-js/issues/4389), since v10.0.0)
 * None.
 
 ### Compatibility

From 9180061eb7987acb21f0af775cadb3d81a997375 Mon Sep 17 00:00:00 2001
From: Tom Duncalf <tom.duncalf@mongodb.com>
Date: Tue, 26 Apr 2022 13:17:15 +0100
Subject: [PATCH 6/6] Remove unnecessary second call to invokeAsync

---
 react-native/ios/RealmReact/RealmReact.mm | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/react-native/ios/RealmReact/RealmReact.mm b/react-native/ios/RealmReact/RealmReact.mm
index dc620a5233..0285be4c7e 100644
--- a/react-native/ios/RealmReact/RealmReact.mm
+++ b/react-native/ios/RealmReact/RealmReact.mm
@@ -351,10 +351,6 @@ - (void)setBridge:(RCTBridge *)bridge {
                     waitingForUiFlush = true;
                     [bridge jsCallInvoker]->invokeAsync([&](){
                         waitingForUiFlush = false;
-
-                        if (waitingForUiFlush) {
-                            [bridge jsCallInvoker]->invokeAsync([&](){});
-                        }
                     });
                 }
             });