Skip to content

Fail gracefully when a component is not registered in production #51176

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<453f8c0a593b173c197fcf54ed834a1b>>
* @generated SignedSource<<0d36e8bfd1d3d3ab4c70d012fda838a3>>
*/

/**
Expand Down Expand Up @@ -132,6 +132,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableFontScaleChangesUpdatingLayout(): Boolean = accessor.enableFontScaleChangesUpdatingLayout()

/**
* Enables gracefuly failure when an unregistered component is rendered in Android.
*/
@JvmStatic
public fun enableGracefulUnregisteredComponentFailureAndroid(): Boolean = accessor.enableGracefulUnregisteredComponentFailureAndroid()

/**
* iOS Views will clip to their padding box vs border box
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a51441451ec25033040ba044ee3371fc>>
* @generated SignedSource<<ceda812b118bfe44039471b6efe14845>>
*/

/**
Expand Down Expand Up @@ -37,6 +37,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableFabricRendererCache: Boolean? = null
private var enableFixForParentTagDuringReparentingCache: Boolean? = null
private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null
private var enableGracefulUnregisteredComponentFailureAndroidCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableJSRuntimeGCOnMemoryPressureOnIOSCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
Expand Down Expand Up @@ -221,6 +222,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableGracefulUnregisteredComponentFailureAndroid(): Boolean {
var cached = enableGracefulUnregisteredComponentFailureAndroidCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableGracefulUnregisteredComponentFailureAndroid()
enableGracefulUnregisteredComponentFailureAndroidCache = cached
}
return cached
}

override fun enableIOSViewClipToPaddingBox(): Boolean {
var cached = enableIOSViewClipToPaddingBoxCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9d0b02395a08331bca956ea600602a31>>
* @generated SignedSource<<95358914a3bb384370c92d1cf7799157>>
*/

/**
Expand Down Expand Up @@ -62,6 +62,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableFontScaleChangesUpdatingLayout(): Boolean

@DoNotStrip @JvmStatic public external fun enableGracefulUnregisteredComponentFailureAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun enableIOSViewClipToPaddingBox(): Boolean

@DoNotStrip @JvmStatic public external fun enableJSRuntimeGCOnMemoryPressureOnIOS(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<cf12cdfdfb343e79247379b5549ae92a>>
* @generated SignedSource<<6a1f3285b5de9165d496c716d0b2c9c4>>
*/

/**
Expand Down Expand Up @@ -57,6 +57,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableFontScaleChangesUpdatingLayout(): Boolean = false

override fun enableGracefulUnregisteredComponentFailureAndroid(): Boolean = false

override fun enableIOSViewClipToPaddingBox(): Boolean = false

override fun enableJSRuntimeGCOnMemoryPressureOnIOS(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<4c81ed8a06c192eb4007219d163650e5>>
* @generated SignedSource<<5b9e7dc6c273440e92f0501f5270a9c9>>
*/

/**
Expand Down Expand Up @@ -41,6 +41,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableFabricRendererCache: Boolean? = null
private var enableFixForParentTagDuringReparentingCache: Boolean? = null
private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null
private var enableGracefulUnregisteredComponentFailureAndroidCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableJSRuntimeGCOnMemoryPressureOnIOSCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
Expand Down Expand Up @@ -242,6 +243,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun enableGracefulUnregisteredComponentFailureAndroid(): Boolean {
var cached = enableGracefulUnregisteredComponentFailureAndroidCache
if (cached == null) {
cached = currentProvider.enableGracefulUnregisteredComponentFailureAndroid()
accessedFeatureFlags.add("enableGracefulUnregisteredComponentFailureAndroid")
enableGracefulUnregisteredComponentFailureAndroidCache = cached
}
return cached
}

override fun enableIOSViewClipToPaddingBox(): Boolean {
var cached = enableIOSViewClipToPaddingBoxCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<2482f57e0652cfaa4806b5333c50ad9f>>
* @generated SignedSource<<33ecc2f26b06289f69a759b1f92602b8>>
*/

/**
Expand Down Expand Up @@ -57,6 +57,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableFontScaleChangesUpdatingLayout(): Boolean

@DoNotStrip public fun enableGracefulUnregisteredComponentFailureAndroid(): Boolean

@DoNotStrip public fun enableIOSViewClipToPaddingBox(): Boolean

@DoNotStrip public fun enableJSRuntimeGCOnMemoryPressureOnIOS(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
import android.content.ComponentCallbacks2;
import android.content.res.Configuration;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -24,6 +28,7 @@
@Nullsafe(Nullsafe.Mode.LOCAL)
public final class ViewManagerRegistry implements ComponentCallbacks2 {

private static final String TAG = "ViewManagerRegistry";
private final Map<String, ViewManager> mViewManagers;
private final @Nullable ViewManagerResolver mViewManagerResolver;

Expand Down Expand Up @@ -76,14 +81,33 @@ public synchronized ViewManager get(String className) {
viewManager = getViewManagerFromResolver(rctViewManagerName);
if (viewManager != null) return viewManager;

throw new IllegalViewOperationException(
String errorMessage =
"Can't find ViewManager '"
+ className
+ "' nor '"
+ rctViewManagerName
+ "' in ViewManagerRegistry"
+ ", existing names are: "
+ mViewManagerResolver.getViewManagerNames());
+ mViewManagerResolver.getViewManagerNames();
// In release mode we don't want to crash the app if the view manager is not found.
// Instead we return a dummy view manager that will render an empty view (including children)
// and log an error.
if (!ReactBuildConfig.DEBUG
&& ReactNativeFeatureFlags.enableGracefulUnregisteredComponentFailureAndroid()) {
// 1. Log the error
FLog.e(TAG, errorMessage);
ReactSoftExceptionLogger.logSoftException(
ReactSoftExceptionLogger.Categories.SOFT_ASSERTIONS,
new IllegalStateException(errorMessage));

// 2. Render UnimplementedNativeView if found.
viewManager = getViewManagerFromResolver("UnimplementedNativeView");
if (viewManager != null) {
return viewManager;
}
}

throw new IllegalViewOperationException(errorMessage);
}
throw new IllegalViewOperationException("No ViewManager found for class " + className);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import android.view.Gravity
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatTextView
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags

internal class ReactUnimplementedView(context: Context) : LinearLayout(context) {
internal class ReactUnimplementedView(
context: Context,
private val onError: ((message: String) -> Unit)
) : LinearLayout(context) {

private val textView: AppCompatTextView = AppCompatTextView(context)

Expand All @@ -33,8 +37,12 @@ internal class ReactUnimplementedView(context: Context) : LinearLayout(context)
}

internal fun setName(name: String) {
val errorMessage = "'$name' is not registered."
if (ReactBuildConfig.DEBUG) {
textView.text = "'$name' is not registered."
textView.text = errorMessage
}
if (ReactNativeFeatureFlags.enableGracefulUnregisteredComponentFailureAndroid()) {
onError(errorMessage)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.facebook.react.views.unimplementedview

import com.facebook.common.logging.FLog
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
Expand All @@ -20,7 +21,11 @@ import com.facebook.react.viewmanagers.UnimplementedNativeViewManagerInterface
* implemented or registered.
*/
@ReactModule(name = ReactUnimplementedViewManager.REACT_CLASS)
internal class ReactUnimplementedViewManager :
internal class ReactUnimplementedViewManager(
private val onError: ((message: String) -> Unit) = { message: String ->
FLog.e(REACT_CLASS, message)
}
) :
ViewGroupManager<ReactUnimplementedView>(),
UnimplementedNativeViewManagerInterface<ReactUnimplementedView> {

Expand All @@ -30,7 +35,7 @@ internal class ReactUnimplementedViewManager :
public override fun getDelegate(): ViewManagerDelegate<ReactUnimplementedView> = delegate

override fun createViewInstance(reactContext: ThemedReactContext): ReactUnimplementedView =
ReactUnimplementedView(reactContext)
ReactUnimplementedView(reactContext, onError)

override fun getName(): String = REACT_CLASS

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<49dbfe02e06cc5d6b12683ed91ea1d13>>
* @generated SignedSource<<f5c20095bd8ddc744ec7d09faea35106>>
*/

/**
Expand Down Expand Up @@ -141,6 +141,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}

bool enableGracefulUnregisteredComponentFailureAndroid() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableGracefulUnregisteredComponentFailureAndroid");
return method(javaProvider_);
}

bool enableIOSViewClipToPaddingBox() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableIOSViewClipToPaddingBox");
Expand Down Expand Up @@ -410,6 +416,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableFontScaleChangesUpdatingLayout(
return ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout();
}

bool JReactNativeFeatureFlagsCxxInterop::enableGracefulUnregisteredComponentFailureAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableGracefulUnregisteredComponentFailureAndroid();
}

bool JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
Expand Down Expand Up @@ -642,6 +653,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enableFontScaleChangesUpdatingLayout",
JReactNativeFeatureFlagsCxxInterop::enableFontScaleChangesUpdatingLayout),
makeNativeMethod(
"enableGracefulUnregisteredComponentFailureAndroid",
JReactNativeFeatureFlagsCxxInterop::enableGracefulUnregisteredComponentFailureAndroid),
makeNativeMethod(
"enableIOSViewClipToPaddingBox",
JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ae23312f2dccee934a8a91c05625662a>>
* @generated SignedSource<<0cf6e6f96fa13fef8ffb65f960a60285>>
*/

/**
Expand Down Expand Up @@ -81,6 +81,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool enableFontScaleChangesUpdatingLayout(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

static bool enableGracefulUnregisteredComponentFailureAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

static bool enableIOSViewClipToPaddingBox(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<8410561a80edd67b4528181b1f8557fe>>
* @generated SignedSource<<d003d3ac6c908343b99a26884b969b5d>>
*/

/**
Expand Down Expand Up @@ -94,6 +94,10 @@ bool ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() {
return getAccessor().enableFontScaleChangesUpdatingLayout();
}

bool ReactNativeFeatureFlags::enableGracefulUnregisteredComponentFailureAndroid() {
return getAccessor().enableGracefulUnregisteredComponentFailureAndroid();
}

bool ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox() {
return getAccessor().enableIOSViewClipToPaddingBox();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<8b135b02d868914f6b3487f09e8955ff>>
* @generated SignedSource<<28a469bce1a0280a46a059187b27f017>>
*/

/**
Expand Down Expand Up @@ -124,6 +124,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static bool enableFontScaleChangesUpdatingLayout();

/**
* Enables gracefuly failure when an unregistered component is rendered in Android.
*/
RN_EXPORT static bool enableGracefulUnregisteredComponentFailureAndroid();

/**
* iOS Views will clip to their padding box vs border box
*/
Expand Down
Loading