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 && <JSX />
renders<JSX />
iffcondition
is truthy, -
condition ? <JsxTrue /> : <JsxFalse />
renders<JsxTrue />
or<JsxFalse />
depending on the truthiness ofcondition
.
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:
- Number
0
likes to leak into your markup. - Compound conditions with
||
can surprise you because precedence - Ternaries don’t scale.
-
props.children
is not something to use as a condition - How to manage update vs remount in conditionals.
If you’re in a hurry, I’ve made a cheat sheet:
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 && <Gallery slides={gallery}>}
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 ? <Gallery slides={gallery} /> : 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 && <div className="error" />
… and I screw up! The code above is actually equivalent to:
user.anonymous || (user.restricted && <div className="error" />)
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) && <div className="error" />}
For a more sneaky case, consider this ternary-inside-the-condition:
{user.registered ? user.restricted : user.rateLimited &&
<div className="error" />}
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 ?
<EmojiButton /> :
isCoupon ?
<CouponButton /> :
isLoaded && <ShareButton />}
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 && <EmojiButton />}
{isCoupon && <CouponButton />}
{!isEmoji && !isCoupon && isLoaded && <ShareButton />}
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 <EmojiButton />;
if (isCoupon) return <CouponButton />;
return isLoaded ? <ShareButton /> : 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 <div>{props.children}</div>
};
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.<Wrap>{[].map(e => <div />)}</Wrap>
-
children.length
fails, too:children
can also be a single element, not an array (<Wrap><div /></Wrap>
). -
React.Children.count(props.children)
supports both single and multiple children, but thinks that<Wrap>{false && 'hi'}{false && 'there'}</Wrap>
contains 2 items, while in reality there are none. - Next try:
React.Children.toArray(props.children)
removes invalid nodes, such asfalse
. Sadly, you still get true for an empty fragment:<Wrap><></><Wrap>
. - For the final nail in this coffin, if we move the conditional rendering inside a component:
<Wrap><Div hide /></Wrap>
withDiv = (p) => p.hide ? null : <div />
, we can never know if it’s empty duringWrap
render, because react only renders the childDiv
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 ? <Item id={1} /> : <Item id={2} />}
What happens when hasItem
changes? Don’t know about you, but my guess would be that <Item id={1} />
unmounts, then <Item id={2} />
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 <Item id={hasItem ? 1 : 2} />
.
When the branches contain different components, as in
{hasItem ? <Item1 /> : <Item2 />}
, React remounts, becauseItem1
can’t be updated to becomeItem2
.
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'
? <input placeholder="name" />
: <input placeholder="phone" />}
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'
? <input placeholder="name" key="name" />
: <input placeholder="phone" key="phone" />}
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' && <input placeholder="name" />}
{mode !== 'name' && <input placeholder="phone" />}
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
<Button
aria-busy={loading}
onClick={loading ? null : submit}
>
{loading ? <Spinner /> : 'submit'}
</Button>
// maybe try:
loading
? <Button aria-busy><Spinner /></Button>
: <Button onClick={submit}>submit</Button>
// or even
{loading &&
<Button key="submit" aria-busy><Spinner /></Button>}
{!loading &&
<Button key="submit" onClick={submit}>submit</Button>}
// ^^ bonus: _move_ the element around the markup, no remount
So, here are my top tips for using JSX conditionals like a boss:
-
{number && <JSX />}
renders0
instead of nothing. Use{number > 0 && <JSX />}
instead. - Don’t forget the parentheses around or-conditions:
{(cond1 || cond2) && <JSX />}
- Ternaries don’t scale beyond 2 branches — try an
&&
block per branch, or extract a function and useif / else
. - You can’t tell if
props.children
(or any interpolated element) actually contains some content — CSS:empty
is your best bet. -
{condition ? <Tag props1 /> : <Tag props2 />}
will not remountTag
— use uniquekey
or separate&&
branches if you want the remount.
Top comments (0)