Conditional rendering in React Native may crash your app
Very soon after start of creating first app every developer needs to render component in one way or another depending on props. When one start searching, the first answer is inside React documentation. The answer is “Conditional Rendering”. But after a while many of us starts facing errors in React Native (or wrong rendering in React for web) that seems to jump on occasionally to reappear some time later. This article will explain what is happening and how to prevent further mistakes.
Conditional rendering using inline If with logical && (AND) operator as React docs says:
Another common shortcut you’ll encounter is the JavaScript logical AND (
&&) operator. Inside React components, it often comes up when you want to render some JSX when the condition istrue, or render nothing otherwise — React docs: Conditional Rendering
{
someValue && (
<View style={styles.field}>
<Text>{someValue}</Text>
</View>
)
}
This handy solution is used by many and it’s nothing new for our community to see in the code. How and why it can crash your React Native app?
When using it widely in your App without proper attention sooner or later you will see this error in your logs (or, on older React Native versions, worse scenario your users would see that the app crashed):
Invariant Violation: Text strings must be rendered within a <Text> component.
(On today’s React Native — Fabric, the New Architecture, which has been the only architecture since React Native 0.82 — this no longer throws. It’s a development-only warning instead. More on what that means for you further down.)
Then you see such an error in your logs and scratch your head because usually it works, it may show up for one particular data entry or after some small API change. What happened? Hint: someValue type matters.
The array example
Another common example of javascript operator wrong usage is rendering something is array contains any elements:
// Sooner or later this code will surprise users.
// Just wait for an empty array.
{
dataEntries.length && (
<View>
<Text>Visible only when array is not empty</Text>
</View>
)
}
Above example looks fine at a first glance. Array’s length will be 0 which is falsy thus condition is not satisfied and following component is not rendered — simple. This reasoning is partially good but author might forgot about one little fact that will surprise users at some point. Let’s take a closer look.
How logical AND && operator in JavaScript works?
Let’s see the docs again:
The logical AND (
&&) (logical conjunction) operator for a set of boolean operands will betrueif and only if all the operands aretrue. Otherwise it will befalse. More generally, the operator returns the value of the first falsy operand encountered when evaluating from left to right, or the value of the last operand if they are all truthy. […] Examples of expressions that can be converted to false are:
false;null;NaN;0;- empty string (
""or''or``);undefined.
Developers love that possibility to treat variables as falsy. Assumption is that when your variable for some reason comes not initialized from backend or other data source you have secured the code from rendering this part of View.
It seems to be a good strategy. We don’t want to show our user nicely formatted undefined string. It’s better to show nothing than null or NaN as well.
Note that description of AND operator says that it returns expr1 or expr2. It always returns one of inputs — not converted. Again: it converts expr1 to Boolean and evaluates result but then returns original value not the converted one. Explained as pseudo code it should look something like this:
if (expr1 == true) {
return expr2
} else {
return expr1
}
Basically it is the whole gotcha but let’s dive into examples. I will use Boolean() JavaScript function to show you how values are converted.
String variable.
Boolean('hello world')
// -> true
Boolean('')
// -> false
Boolean(' ') // space
// -> true
'' && 'conditionally returned string'
// -> ''
'hello world' && 'conditionally returned string'
// -> 'conditionally returned string'
Empty string is falsy so AND operator will return '' because the condition is not fulfilled. In React Native using React versions before 18, returning '' as a child outside a <Text> component produced Text strings must be rendered within a <Text> component and caused a crash. In React on the web, the same empty string produced no visible content and did not cause this error. Since React 18, the reconciler explicitly skips empty strings when reconciling children (typeof newChild === 'string' && newChild !== ''), so '' is treated as a hole in the tree and renders nothing on either platform. React Native picked this up with React Native 0.69, the first release bundling React 18.
Numeric variable.
Boolean(-1)
// -> true
Boolean(0)
// -> false
Boolean(1)
// -> true
0 && 'conditionally returned string'
// -> 0
1 && 'conditionally returned string'
// -> 'conditionally returned string'
Zero is falsy so logical AND operator will return 0 as the condition is not met. Unlike the empty string, 0 (and NaN) has no special case in React’s reconciler — it still leaks straight into the tree. On today’s React Native, though, this no longer throws Invariant Violation: on Fabric, the “Text strings must be rendered within a <Text> component” check is a development-only console.error (you’ll see it as a LogBox warning), and it is silently skipped in production builds — so your app won’t crash, it will just quietly render a stray 0 (or nothing at all) with no error to tell you why.
Other variable types worth mentioning.
Boolean(null)
// -> false
Boolean(undefined)
// -> false
Boolean({})
// -> true
Boolean([]) // worth noting!
// -> true
From the above examples the most interesting from React Native developer’s point of view is array. Usually when we put array into conditional render we would like not to render anything if array is empty. Passing an empty array into logical expression without any preparation will mislead us. What one would need to do is to check whether length exists and is equal to 0.
Why React Native crashes?
Rendering string in React Native must be wrapped with <Text>...</Text> component. But when we want to hide entire component when variable is empty with conditional rendering it may return an empty string directly into JSX. For example:
let optionalStr = ''
// [...] some logic that leaves `optionalStr` empty
{
optionalStr && <Text>{optionalStr}</Text>
} // renders nothing since React 18 — swap in 0 or NaN and it still leaks
Now you know that above condition is not fulfilled therefore logical AND operator will return optionalStr directly into main JSX. If optionalStr were 0 or NaN instead of '', that leaked value would still reach the tree today — React 18+ only special-cases the empty string, not other falsy values.
What about a numeric variable?
Don’t put numbers on the left side of
&&. To test the condition, JavaScript converts the left side to a boolean automatically. However, if the left side is0, then the whole expression gets that value (0), and React will happily render0rather than nothing. — React docs: Conditional Rendering
React tries to convert results of your expressions into a string, React element or array, so a leaked Number gets rendered as text just like a leaked string. This is why you used to see Invariant Violation: Text strings must be rendered within a <Text> component even if your variable was a Number — on current React Native this instead surfaces as a development-only warning (see below), which can be even more misleading while chasing this bug, since production gives you no error at all.
Why is it hard to find React Native conditional render error?
This error is sneaky because it may take a long time before it shows up. You code may be working like a charm without any issues for months and suddenly something changes on API and type of that nullable variable changes suddenly to empty string or 0.
It got quieter, which makes it sneakier, not safer. Since React 18, an empty string leaking out of && is a non-issue — it renders nothing, full stop. But 0 and NaN still leak straight into the tree, and on current React Native (Fabric) the only signal you get is a development-only console.error/LogBox warning. There is no thrown error any more, in dev or in production. That means the bug that used to announce itself loudly with a red screen now ships silently: your production users just see a stray 0 (or missing content) with nothing in your crash reporting to point you at it.
Why it works with variables that are null or undefined then? It will also work for booleans. React creators make our life easier and by default such variables are ignored in a JSX tree. It is special case and it will not be rendered.
React will also not crash when you put empty array directly into JSX as arrays can render multiple elements.
// below expressions will not crash your React Native app
<View>
{false}
{true}
{null}
{undefined}
{[]}
</View>
React for web — zero appears
Developing a website in React and not converting variable into boolean will break things in much the same way. The empty-string skip described above lives in the shared reconciler, so '' renders nothing on web too. A leaked 0, though, is a normal string as far as HTML is concerned, so it gets rendered — that’s when some strange 0 appears on the site. These days the practical difference between the two platforms is small: neither crashes, and on both you’re left hunting for a stray character with no error to guide you.
How to make conditional rendering safer?
Just make sure to convert every variable into Boolean before using logical AND && operator.
You can do it multiple ways:
Double negation — !!dangerousData
It’s an easy quick fix that will work and some experiments says that it’s execution time is faster than Boolean(dangerousData).
I do not recommend it though.
This solution’s main pitfall is a human factor. Someone in your team could think that it is pointless to do double negation as it goes from true -> false -> true. It may lead to “refactor” that will create potential crashes in the future as this error may not reveal itself at first. My number one principle while coding is readability.
Classic conversion — Boolean(dangerousData)
This seems readable but as I mentioned above some say that it is slower in execution time so make your own research and decide if it is OK for your particular case. We can find news that in modern browsers it is optimized. You may also use some transpilers to change it before it goes to final code.
Rethink components architecture.
Maybe you don’t need as many conditional renders in the component. Every component should be small and have simplified logic as much as it can. I have seen many overly complicated components with nested conditional renders and believe me it’s not something easy to maintain as your code grows.
Use Element variable
In simple components sometimes you can use trick from React documentation with if and variable assignment preceding return.
// ...
let message = <Text>'Hello there!'</Text>
if (isVillain) {
message = <Text style={styles.deepVoice}>'General React'oni!'</Text>
}
return <View>{message}</View>
Component is a function (if else in render)
In class components it would be — render method is a function.
In function, you can call return inside if statement and it will not execute further on. It will have the same result as with Element variable above. We don’t need else here because when condition is satisfied execution will go on, otherwise it will be stopped on first render.
// ...
if (isVillain) {
return (
<View>
<Text style={styles.deepVoice}>'General React'oni!'</Text>
</View>
)
}
return (
<View>
<Text>'Hello there!'</Text>
</View>
)
Conditional (ternary) operator
You can also use conditional operator (ternary expression) condition ? passed : failed but be aware that nesting those will destroy readability of your code. My advice is to set up no-nested-ternary rule for ESLint otherwise your code can become this: const thing = foo ? bar : baz === qux ? quxx : foobar; but with lots more of code because components rises very quick in amount of letters. Multiple elements inside nested ternary operator will make render complicated and unreadable.
// ...
return (
<View>
{isVillain ? (
<Text style={styles.deepVoice}>'General React'oni!'</Text>
) : (
<Text>'Hello there!'</Text>
)}
</View>
)
Let the linter catch this for you
If remembering to convert every variable feels error-prone, it’s because it is — so don’t rely on remembering it. The ESLint rule react/jsx-no-leaked-render from eslint-plugin-react flags exactly this pattern and reports it with the message Potential leaked value that might cause unintentionally rendered values or rendering crashes. It’s autofixable, offering both the !!/Boolean() coercion and the ternary rewrite shown above.
Explaining the array example (from the introduction)
Just to remind you I was showing this example:
{
dataEntries.length && (
<View>
<Text>Visible only when array is not empty</Text>
</View>
)
}
Now you understand that what really happens in above code is returning length to directly into JSX. It happens when length is falsy and it comes from logical operator implementation.
To simplify the example and make things more visible lets assume that dataEntries.length is 0 and following View with Text component is <Component />. Now we have:
{
0 && <Component />
}
This expression returns 0 which is converted to string '0'. On the web that just shows up as an extra character. On React Native it still leaks into the tree the same way — the difference is that on today’s Fabric-based React Native the “Text strings must be rendered within a <Text> component” check only logs a development-only warning, so you’ll see it in your logs while developing but not as an error in production.
The quickest fix possible is to make sure that we don’t depend on falsy value but on boolean false.
Here are multiple fix scenarios:
Double negation
{
!!dataEntries.length && <Component />
}
Classic conversion
{
Boolean(dataEntries.length) && <Component />
}
Inline condition
{
dataEntries.length > 0 && <Component />
}
Ternary operator
{
dataEntries.length ? <Component /> : null
}
Refactor, rethink, make it safe
let coditionalComponent = null
if (dataEntries.length > 0) {
coditionalComponent = <Component />
}
One place this is not a problem: style arrays
Worth calling out so you don’t over-apply the fix: inside a style array, style={[styles.a, dataEntries.length && styles.b]} is safe. React Native’s flattenStyle walks the array and skips any entry that isn’t an object (false, undefined, 0, '', NaN all get dropped with a plain continue), so a falsy leak there never reaches a <Text> child the way it does when the same leaked value is put among children.
Do you have other way to render on specific condition? Write it on Twitter or comments under this article. Let’s talk about your observations with this problem.