Guard Rendered JSX With A Boolean
ID |
javascript.jsx_conditional_render_non_boolean |
Severity |
low |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
jsx, react, reliability |
Description
Reports the {value && <Element />} idiom when the guard is a number rather than a condition. React
renders the guard’s falsy value, and a falsy number is a visible 0.
Rationale
&& in JavaScript does not return a boolean: it returns its left operand when that operand is
falsy. In JSX children the returned value is then rendered, and React renders every number it is
given — including 0 and NaN.
So a guard on a count works for every count but the one that matters: with an empty list the page
shows a bare 0 where the list should be. It is the most-reported React rendering bug there is, and
it survives review because the line reads as a condition.
Only guards this scanner can see the type of are reported: a numeric literal, and a read of .length
or .size, whose type is fixed by the language whatever the receiver is. A plain identifier is left
alone — count may well be a number, but the rule would then be guessing, and a wrong guess here
lands on correct code. A string literal guard is reported too, for a different reason: its
truthiness is constant, so the guard is not a guard.
The check applies to a guard whose value reaches JSX children, which is where it would be rendered.
The same expression in an attribute or in plain JavaScript is ordinary code. Chained guards nest to
the right, and each && in the chain is judged on its own left operand, so the numeric one is found
wherever in the chain it sits.
<div>{items.length && <List items={items} />}</div> // FLAW - an empty list renders "0"
<div>{selected.size && <Toolbar />}</div> // FLAW - same for a Set or a Map
<div>{0 && <List />}</div> // FLAW - renders "0", always
<div>{items.length > 0 && <List items={items} />}</div> // OK - a comparison is a boolean
<div>{!!items.length && <List items={items} />}</div> // OK - coerced
<div>{isOpen && <Toolbar />}</div> // OK - reads as a condition
<div>{items.length ? <List /> : null}</div> // OK - a conditional picks a branch
Remediation
Make the guard a boolean. Compare the count (items.length > 0), coerce it (!!items.length), or
switch to a conditional expression, which picks a branch instead of returning the guard
(items.length ? <List /> : null). The comparison is usually the clearest of the three, because it
says what the empty case is.