diff --git a/packages/react-native-renderer/src/AssertTextIsInTextComponent.js b/packages/react-native-renderer/src/AssertTextIsInTextComponent.js new file mode 100644 index 0000000000000..f78789784e574 --- /dev/null +++ b/packages/react-native-renderer/src/AssertTextIsInTextComponent.js @@ -0,0 +1,33 @@ +import invariant from 'shared/invariant'; + +export type HostContext = $ReadOnly<{| + isInAParentText: boolean, +|}>; + +function stripInformation(internalInstanceHandle: Object) { + const possibleCause = + '\n\nProbably result of a conditional rendering using boolean concatination as in `cond && `.'; + if (internalInstanceHandle && internalInstanceHandle.sibling) { + const debugOwner = internalInstanceHandle.sibling._debugOwner; + const debugSource = internalInstanceHandle.sibling._debugSource; + if (debugOwner && debugSource) { + const parentComponentName = debugOwner.type.name; + const siblingSource = `"${debugSource.fileName}" line ${debugSource.lineNumber}, column ${debugSource.columnNumber}`; + return ` Error may have occured in component <${parentComponentName}> near ${siblingSource}. ${possibleCause}`; + } + } + return possibleCause; +} + +export function assertTextInTextComponent( + hostContext: HostContext, + text: string, + internalInstanceHandle: Object, +) { + invariant( + hostContext.isInAParentText, + 'Text string "%s" must be rendered within a component.%s', + text, + stripInformation(internalInstanceHandle), + ); +} diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 27b7076377fc4..34c84c100897e 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -20,6 +20,7 @@ import type { import {mountSafeCallback_NOT_REALLY_SAFE} from './NativeMethodsMixinUtils'; import {create, diff} from './ReactNativeAttributePayload'; +import {assertTextInTextComponent} from './AssertTextIsInTextComponent'; import {enableNewReconciler} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; @@ -250,10 +251,7 @@ export function createTextInstance( hostContext: HostContext, internalInstanceHandle: Object, ): TextInstance { - invariant( - hostContext.isInAParentText, - 'Text strings must be rendered within a component.', - ); + assertTextInTextComponent(hostContext, text, internalInstanceHandle); const tag = nextReactTag; nextReactTag += 2; diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index 7f11358556287..f7c2b5e1a8ba4 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -29,6 +29,7 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent'; import {DefaultLanePriority as DefaultLanePriority_old} from 'react-reconciler/src/ReactFiberLane.old'; import {DefaultLanePriority as DefaultLanePriority_new} from 'react-reconciler/src/ReactFiberLane.new'; +import {assertTextInTextComponent} from './AssertTextIsInTextComponent'; const DefaultLanePriority = enableNewReconciler ? DefaultLanePriority_new @@ -152,10 +153,7 @@ export function createTextInstance( hostContext: HostContext, internalInstanceHandle: Object, ): TextInstance { - invariant( - hostContext.isInAParentText, - 'Text strings must be rendered within a component.', - ); + assertTextInTextComponent(hostContext, text, internalInstanceHandle); const tag = allocateTag(); diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index a935da5e4df63..2066174ad7c6d 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -625,7 +625,7 @@ describe('ReactFabric', () => { })); expect(() => ReactFabric.render(this should warn, 11)).toThrow( - 'Text strings must be rendered within a component.', + 'Text string "this should warn" must be rendered within a component.\n\nProbably result of a conditional rendering using boolean concatination as in `cond && `.', ); expect(() => @@ -635,7 +635,9 @@ describe('ReactFabric', () => { , 11, ), - ).toThrow('Text strings must be rendered within a component.'); + ).toThrow( + 'Text string "hi hello hi" must be rendered within a component.\n\nProbably result of a conditional rendering using boolean concatination as in `cond && `.', + ); }); it('should not throw for text inside of an indirect ancestor', () => { diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 9026bef793767..f2c1b816e4338 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -488,7 +488,7 @@ describe('ReactNative', () => { })); expect(() => ReactNative.render(this should warn, 11)).toThrow( - 'Text strings must be rendered within a component.', + 'Text string "this should warn" must be rendered within a component.\n\nProbably result of a conditional rendering using boolean concatination as in `cond && `.', ); expect(() => @@ -498,7 +498,9 @@ describe('ReactNative', () => { , 11, ), - ).toThrow('Text strings must be rendered within a component.'); + ).toThrow( + 'Text string "hi hello hi" must be rendered within a component.\n\nProbably result of a conditional rendering using boolean concatination as in `cond && `.', + ); }); it('should not throw for text inside of an indirect ancestor', () => { diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index e758c79be2e58..3c1882470e526 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -373,5 +373,6 @@ "382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.", "383": "This query has received fewer parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.", "384": "Refreshing the cache is not supported in Server Components.", - "385": "A mutable source was mutated while the %s component was rendering. This is not supported. Move any mutations into event handlers or effects." + "385": "A mutable source was mutated while the %s component was rendering. This is not supported. Move any mutations into event handlers or effects.", + "386": "Text string \"%s\" must be rendered within a component.%s" }