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).
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;
};
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.React.Children.toArray(props.children)
removes invalid nodes, such as false
. Sadly, you still get true for an empty fragment: <Wrap><></><Wrap>
.<Wrap><Div hide /></Wrap>
with Div = (p) => p.hide ? null : <div />
, 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.
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 />}
renders 0
instead of nothing. Use {number > 0 && <JSX />}
instead.{(cond1 || cond2) && <JSX />}
&&
block per branch, or extract a function and use if / else
.props.children
(or any interpolated element) actually contains some content — CSS :empty
is your best bet.{condition ? <Tag props1 /> : <Tag props2 />}
will not remount Tag
— use unique key
or separate &&
branches if you want the remount.