React Conditional Rendering Patterns
Conditional rendering in React looks easy until the `0`-falsy-render bug ships to production. This snippet covers the three patterns you should reach for in order: a ternary in JSX for either-or branches, the `&&` short-circuit (with the famous `0` gotcha and its fix), and an early-return plus extracted helper component when the branching gets dense. Examples render a small text representation of the output so the teaching is independent of any JSX compiler.
806 views
25
// Standalone JS demo. In real React the bodies would be JSX; we use a tiny
// `el(tag, children)` helper that mimics what React.createElement would produce
// and a `render(node)` that turns the node tree into a printable string.
function el(tag, children) { return { tag, children }; }
function render(node) {
if (node === null || node === undefined || node === false) return '';
if (typeof node === 'string' || typeof node === 'number') return String(node);
const inner = (node.children || []).map(render).join('');
return `<${node.tag}>${inner}</${node.tag}>`;
}
// Welcome banner that flips between two messages based on isLoggedIn.
function Welcome({ isLoggedIn, name }) {
return el('div', [
isLoggedIn
? el('span', ['Welcome back, ', name, '!'])
: el('a', ['Sign in']),
]);
}
console.log(render(Welcome({ isLoggedIn: true, name: 'Ada' })));
// <div><span>Welcome back, Ada!</span></div>
console.log(render(Welcome({ isLoggedIn: false, name: 'Ada' })));
// <div><a>Sign in</a></div>
// Returning null is React's way to render nothing without breaking the tree.
function Banner({ message }) {
return message ? el('div', [message]) : null;
}
console.log(render(Banner({ message: '' })));
// (empty string)
console.log(render(Banner({ message: 'Hello' })));
// <div>Hello</div>A ternary in JSX is the cleanest way to express "either A or B": both branches are explicit, and the result is always a valid React node. When you want "render this only sometimes", returning null (or false, or undefined) tells React to render nothing, which is safer than building an empty <div>. The el/render pair here mimics what React.createElement and the renderer do under the hood so we can run the example in plain Node; in your real component you would write <span>Welcome back, {name}!</span> instead. Keep ternaries short; nested ternaries hurt readability fast and should be refactored to early returns or a helper component.
function el(tag, children) { return { tag, children }; }
function render(node) {
if (node === null || node === undefined || node === false) return '';
if (typeof node === 'string' || typeof node === 'number') return String(node);
const inner = (node.children || []).map(render).join('');
return `<${node.tag}>${inner}</${node.tag}>`;
}
// BUG: when count is 0, `count && <Cart />` evaluates to 0, not false.
// React (and our `render` helper) actually print the number 0 instead of nothing.
function CartBadgeBuggy({ count }) {
return el('header', [count && el('span', ['Cart: ', count])]);
}
console.log(render(CartBadgeBuggy({ count: 0 })));
// <header>0</header> <- a stray '0' leaks into the output
console.log(render(CartBadgeBuggy({ count: 3 })));
// <header><span>Cart: 3</span></header>
// FIX 1: explicit comparison so the left side is always a boolean.
function CartBadgeFixed({ count }) {
return el('header', [count > 0 && el('span', ['Cart: ', count])]);
}
console.log(render(CartBadgeFixed({ count: 0 })));
// <header></header>
console.log(render(CartBadgeFixed({ count: 3 })));
// <header><span>Cart: 3</span></header>
// FIX 2: cast with Boolean(...) when the left side is varied or unclear.
function CartBadgeBool({ items }) {
return el('header', [Boolean(items?.length) && el('span', ['Items in cart'])]);
}
console.log(render(CartBadgeBool({ items: [] })));
// <header></header>
console.log(render(CartBadgeBool({ items: [1, 2] })));
// <header><span>Items in cart</span></header>A && B returns B when A is truthy and returns A itself when A is falsy. That second clause is the trap: if A is the number 0, && returns 0, and React happily renders 0 as a text node. The two safe fixes are to compare to a real boolean (count > 0) or to cast (Boolean(items?.length)); both ensure the left operand is true or false, never 0. The same problem applies to empty strings ('') and NaN; if the left side might ever be falsy-but-printable, prefer a ternary or a comparison. The takeaway: && is shorthand for "this thing is a boolean flag", not "this thing is non-zero".
function el(tag, children) { return { tag, children }; }
function render(node) {
if (node === null || node === undefined || node === false) return '';
if (typeof node === 'string' || typeof node === 'number') return String(node);
const inner = (node.children || []).map(render).join('');
return `<${node.tag}>${inner}</${node.tag}>`;
}
// Helper component owns one branch each, keeping the parent flat.
function LoadingState() { return el('div', ['Loading...']); }
function ErrorState({ message }) { return el('div', ['Error: ', message]); }
function EmptyState() { return el('div', ['No results yet.']); }
function ResultsList({ items }) {
return el('ul', items.map((it) => el('li', [it])));
}
// Parent uses early returns instead of stacking ternaries inside JSX.
function SearchResults({ status, error, items }) {
if (status === 'loading') return LoadingState();
if (status === 'error') return ErrorState({ message: error });
if (items.length === 0) return EmptyState();
return ResultsList({ items });
}
console.log(render(SearchResults({ status: 'loading', items: [] })));
// <div>Loading...</div>
console.log(render(SearchResults({ status: 'error', error: 'timeout', items: [] })));
// <div>Error: timeout</div>
console.log(render(SearchResults({ status: 'ok', items: [] })));
// <div>No results yet.</div>
console.log(render(SearchResults({ status: 'ok', items: ['a', 'b'] })));
// <ul><li>a</li><li>b</li></ul>When a component has more than two branches, ternaries inside JSX become unreadable; early returns turn the branching into a plain switch-like cascade that is easy to scan top to bottom. Extracting each branch to a small named helper (LoadingState, ErrorState, EmptyState) lets the parent stay declarative and tells the reader at a glance what each state looks like. This also unlocks easy reuse: another search page can pull EmptyState directly without copy-pasting markup. Avoid the temptation to inline if/else blocks inside JSX with an IIFE; if you ever feel the need, that is a strong signal that the branches deserve to be their own components.
