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:
You may embed any expressions in JSX by wrapping them in curly braces. This includes the JavaScript logical
&&
operator. It can be handy for conditionally including an element — 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 (or worse scenario your users will see that the app crashed):
Invariant Violation: Text strings must be rendered within a <Text> component.
Then you see such an error in your logs and scratch your head because usually it works, it may crash 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 may 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:
Operator Syntax Description Logical AND ( &&
)expr1 && expr2
If expr1
can be converted totrue
, returnsexpr2
; else, returnsexpr1
.[…]If a value can be converted to
true
, the value is so-called truthy. If a value can be converted tofalse
, the value is so-called falsy.Examples of expressions that can be converted to false are:
null
;NaN
;0
;- empty string (
''
[..]);undefined
.
Developers love that possibility to treat variables as falsy. Assumption is that when your variable for some reason comes not initialised 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. Returning ''
directly into ReactNative JSX will produce error Text strings must be rendered within a <Text> component
and cause crash.
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. Returning 0
into ReactNative JSX will cause crash with Invariant Violation
error again.
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>} // crash
Now you know that above condition is not fulfilled therefore logical AND operator will return optionalStr
directly into main JSX.
What about a numeric variable?
Normally, JavaScript expressions inserted in JSX will evaluate to a string, a React element, or a list of those things. […] — React docs: JSX in Depth
React tries to convert results of your expressions into a string, React element or array. This is why you see Invariant Violation: Text strings must be rendered within a <Text> component
even if your variable was Number
. It may be misleading while searching for this bug in a production code.
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
.
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 also break things but not as much as on native platform. For web empty string or 0 will be rendered. It is normal string and those can be rendered in HTML. For empty string it is usually missed and everything works well as nothing appears on the screen. It may be spotted when one try to conditionally render numeric variable as some strange 0 appears on the site. But nothing crashes and users are not upset as much.
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 upno-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>
)
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'
and you can see it as an error in React Native or as an extra character on the web.
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 conditionalComponent = null
if(dataEntries.length > 0){
conditionalComponent = <Component />
}
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.
Top comments (0)