https://thoughtspile.github.io/2022/01/17/jsx-conditionals/
Vladimir Klepov as a Coder
Good advice on JSX conditionals
Conditional rendering is a cornerstone of any templating language.
React / JSX bravely chose not to have a dedicated conditional syntax,
like ng-if="condition", relying on JS boolean operators instead:
* condition && renders iff condition is truthy,
* condition ? : renders or
depending on the truthiness of condition.
Courageous, but not always as intuitive as you'd expect. Time after
time I shoot myself in the foot with JSX conditionals. In this
article, I look at the trickier corners of JSX conditionals, and
share some tips for staying safe:
1. Number 0 likes to leak into your markup.
2. Compound conditions with || can surprise you because precedence
3. Ternaries don't scale.
4. props.children is not something to use as a condition
5. How to manage update vs remount in conditionals.
If you're in a hurry, I've made a cheat sheet:
[jsx-condit]
Beware of zero
Rendering on numerical condition is a common use case. It's helpful
for rendering a collection only if it's loaded and non-empty:
{gallery.length && }
However, if the gallery is empty, we get an annoying 0 in out DOM
instead of nothing. That's because of the way && works: a falsy
left-hand side (like 0) is returned immediately. In JS, boolean
operators do not cast their result to boolean -- and for the better,
since you don't want the right-hand JSX to turn into true. React then
proceeds to put that 0 into the DOM -- unlike false, it's a valid
react node (again, for good -- in you have {count} tickets rendering 0
is perfectly expected).
The fix? I have two. Cast the condition to boolean explicitly in any
way you like. Now the expression value is false, not 0, and false is
not rendered:
gallery.length > 0 && jsx
// or
!!gallery.length && jsx
// or
Boolean(gallery.length) && jsx
Alternatively, replace && with a ternary to explicitly provide the
falsy value -- null works like a charm:
{gallery.length ? : null}
Mind the precedence
And (&&) has a higher precedence than or (||) -- that's how boolean
algebra works. However, this also means that you must be very careful
with JSX conditions that contain ||. Watch as I try to render an
access error for anonymous or restricted users...
user.anonymous || user.restricted &&
... and I screw up! The code above is actually equivalent to:
user.anonymous || (user.restricted && )
Which is not what I want. For anonymous users, you get true ||
...whatever..., which is true, because JS knows the or-expression is
true just by looking at the left-hand side and skips (short-circuits)
the rest. React doesn't render true, and even if it did, true is not
the error message you expect.
As a rule of thumb, parenthesize the condition as soon as you see the
OR:
{(user.anonymous || user.restricted) && }
For a more sneaky case, consider this ternary-inside-the-condition:
{user.registered ? user.restricted : user.rateLimited &&
}
Parentheses still help, but avoiding ternaries in conditions is a
better option -- they're very confusing, because you can't even read
the expression out in English (if the user is registered then if the
user is restricted otherwise if the user is rate-limited, please make
it stop).
Don't get stuck in ternaries
A ternary is a fine way to switch between two pieces of JSX. Once you
go beyond 2 items, the lack of an else if () turns your logic into a
bloody mess real quick:
{isEmoji
?
: isCoupon
?
: isLoaded && }
Any extra conditions inside a ternary branch, be it a nested ternary
or a simple &&, are a red flag. Sometimes, a series of && blocks
works better, at the expense of duplicating some conditions:
{isEmoji && }
{isCoupon && }
{!isEmoji && !isCoupon && isLoaded && }
Other times, a good old if / else is the way to go. Sure, you can't
inline these in JSX, but you can always extract a function:
const getButton = () => {
if (isEmoji) return ;
if (isCoupon) return ;
return isLoaded ? : null;
};
Don't conditional on JSX
In case you're wondering, react elements passed via props don't work
as a condition. Let me try wrapping the children in a div only if
there are children:
const Wrap = (props) => {
if (!props.children) return null;
return
{props.children}
};
I expect Wrap to render null when there is no content wrapped, but
React doesn't work like that:
* props.children can be an empty array, e.g. {[].map(e =>
)}
* children.length fails, too: children can also be a single
element, not an array ().
* React.Children.count(props.children) supports both single and
multiple children, but thinks that {false && 'hi'}{false &&
'there'} contains 2 items, while in reality there are
none.
* Next try: React.Children.toArray(props.children) removes invalid
nodes, such as false. Sadly, you still get true for an empty
fragment: <>>.
* For the final nail in this coffin, if we move the conditional
rendering inside a component: with Div
= (p) => p.hide ? null : , we can never know if it's empty
during Wrap render, because react only renders the child Div
after the parent, and a stateful child can re-render
independently from its parent.
For the only sane way to change anything if the interpolated JSX is
empty, see CSS :empty pseudo-class.
Remount or update?
JSX written in separate ternary branches feels like completely
independent code. Consider the following:
{hasItem ? : }
What happens when hasItem changes? Don't know about you, but my guess
would be that unmounts, then mounts,
because I wrote 2 separate JSX tags. React, however, doesn't know or
care what I wrote, all it sees is the Item element in the same
position, so it keeps the mounted instance, updating props (see
sandbox). The code above is equivalent to .
When the branches contain different components, as in {hasItem ?
: }, React remounts, because Item1 can't be
updated to become Item2.
The case above just causes some unexpected behavior that's fine as
long as you properly manage updates, and even a bit more optimal than
remounting. However, with uncontrolled inputs you're in for a
disaster:
{mode === 'name'
?
: }
Here, if you input something into name input, then switch the mode,
your name unexpectedly leaks into the phone input. (again, see
sandbox) This can cause even more havoc with complex update mechanics
relying on previous state.
One workaround here is using the key prop. Normally, we use it for
rendering lists, but it's actually an element identity hint for React
-- elements with the same key are the same logical element.
// remounts on change
{mode === 'name'
?
: }
Another option is replacing the ternary with two separate && blocks.
When key is absent, React falls back to the index of the item in
children array, so putting distinct elements into distinct positions
works just as well as an explicit key:
{mode === 'name' && }
{mode !== 'name' && }
Conversely, if you have very different conditional props on the same
logical element, you can split the branching into two separate JSX
tags for readability with no penalty:
// messy
// maybe try:
{loading
?
: }
// or even
{loading && }
{!loading && }
// ^^ bonus: _move_ the element around the markup, no remount
---------------------------------------------------------------------
So, here are my top tips for using JSX conditionals like a boss:
* {number && } renders 0 instead of nothing. Use {number > 0
&& } instead.
* Don't forget the parentheses around or-conditions: {(cond1 ||
cond2) && }
* Ternaries don't scale beyond 2 branches -- try an && block per
branch, or extract a function and use if / else.
* You can't tell if props.children (or any interpolated element)
actually contains some content -- CSS :empty is your best bet.
* {condition ? : } will not remount
Tag -- use unique key or separate && branches if you want the
remount.
Tweet
Share
Discuss on Twitter
More? All articles ever frontend hooks programming react
Written in 2022 by your friend, Vladimir. Follow me on Twitter to get
post updates. I have RSS, too. And you can buy me a coffee!
Older
Make useRef lazy -- 4 ways
Newer
What is a react component, anyways?