From 508bc843f9682243c8ba4f8619497dae476c57a4 Mon Sep 17 00:00:00 2001 From: Joe Mun Date: Thu, 17 Sep 2026 15:34:36 -0700 Subject: [PATCH 1/2] [Discord] Build the constants native map on first request Constructing a WritableNativeMap loads React Native's native library through SoLoader. The cache's loading thread starts when the host app calls init, so its ordering against SoLoader.init is the host's to determine, not something this class can assume. The map is therefore built in getUIManagerConstantsAsWritableMap, once, on the first call - which comes from JS requesting UIManager constants, after that library is loaded. --- .../uimanager/UIManagerConstantsCache.java | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java index 074d57e806ae..fd4a02901bba 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java @@ -27,8 +27,7 @@ * On init(...), a background thread: * 1) calls MMKV.initialize(...) * 2) reads both keys, parses them into Maps - * 3) builds a WritableNativeMap from the full constants map via Arguments.makeNativeMap(...) - * 4) countDown()s loadLatch + * 3) countDown()s loadLatch * * Exposed methods all block until that single background load finishes: * • getCachedConstants() → Map or null @@ -80,8 +79,7 @@ public static UIManagerConstantsCache getInstance() { * constants are accessed. This kicks off: * 1) MMKV.initialize(...) * 2) A background thread that reads two MMKV keys, parses them → Maps, - * then builds a WritableNativeMap from the full constants, and finally - * countDown()s loadLatch. + * and countDown()s loadLatch. */ public synchronized void init(Context appContext) { if (initCalled) { @@ -173,15 +171,6 @@ public synchronized void init(Context appContext) { } Log.v(TAG, "No bubblingEventTypes found in MMKV."); } - - // 2c) Build WritableNativeMap from full constants (if available) - synchronized (this) { - if (cachedConstants != null) { - cachedNativeMap = Arguments.makeNativeMap(cachedConstants); - } else { - cachedNativeMap = null; - } - } } finally { // Signal load completion loadLatch.countDown(); @@ -224,8 +213,14 @@ public Map getCachedBubblingEventsTypes() { } /** - * Blocks until background-load (and WritableNativeMap build) finishes. - * @return pre-built WritableNativeMap or null if no full-constants available. + * Blocks until the background load finishes, then returns the full constants as a + * WritableNativeMap, building it on the first call. + * + * The map is built here rather than on the loading thread because constructing a + * WritableNativeMap loads React Native's native library, which callers of this method + * already require. + * + * @return WritableNativeMap or null if no full-constants available. */ public WritableNativeMap getUIManagerConstantsAsWritableMap() { try { @@ -236,6 +231,17 @@ public WritableNativeMap getUIManagerConstantsAsWritableMap() { return null; } synchronized (this) { + if (cachedNativeMap == null && cachedConstants != null) { + try { + cachedNativeMap = Arguments.makeNativeMap(cachedConstants); + } catch (RuntimeException e) { + // The cached shape cannot be converted, so drop it and let the caller + // regenerate. Native load failures propagate: the caller's fallback builds a + // WritableNativeMap too, so it cannot recover from them either. + Log.w(TAG, "Failed to build WritableNativeMap from cached constants.", e); + cachedConstants = null; + } + } return cachedNativeMap; } } From eda04286c3f7c3f95e09ef2bc0c2dbabe0115317 Mon Sep 17 00:00:00 2001 From: Joe Mun Date: Thu, 17 Sep 2026 17:35:37 -0700 Subject: [PATCH 2/2] Drop the conversion guard The JSON decoder only emits types Arguments.makeNativeMap converts, so the guard covered nothing reachable. It also cleared the in-memory map while leaving the MMKV blob and its version key in place, so the next start would have read and failed on the same data. --- .../react/uimanager/UIManagerConstantsCache.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java index fd4a02901bba..827fa04e203a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java @@ -232,15 +232,7 @@ public WritableNativeMap getUIManagerConstantsAsWritableMap() { } synchronized (this) { if (cachedNativeMap == null && cachedConstants != null) { - try { - cachedNativeMap = Arguments.makeNativeMap(cachedConstants); - } catch (RuntimeException e) { - // The cached shape cannot be converted, so drop it and let the caller - // regenerate. Native load failures propagate: the caller's fallback builds a - // WritableNativeMap too, so it cannot recover from them either. - Log.w(TAG, "Failed to build WritableNativeMap from cached constants.", e); - cachedConstants = null; - } + cachedNativeMap = Arguments.makeNativeMap(cachedConstants); } return cachedNativeMap; }