Conditions: empty, error, loading
In short: Conditional rendering shows only the part of the markup that matches the data (loading, error, empty or the list); in React with && and the ternary operator.
Problem
Loading, error and the list at once
The catalog has loaded. Under the heading, though, it says "Loading…", then "The products did not load.", then "There are no products in this category.", and only then come the products.
Console
The Catalog component gets status and the products as props. But it does not look at them: it always draws all four parts. The buyer does not know what to believe, and a screen reader reads both the error and the products.
The idea
The part that matches the data
The catalog has four states: loading, error, empty and ready. Only the part for the current one should be on the screen. This is conditional rendering: the component decides what to return based on the data.
JSX has three ways, depending on how much changes:
// A whole different view: if and an early returnif (status === 'loading') return <p role="status">Loading…</p>;
// One of two: ? :<p>{product.stock === 0 ? 'Sold out' : 'In stock'}</p>
// Something or nothing: &&{status === 'error' && <p role="alert">The products did not load.</p>}if is plain JavaScript: the component is a function and can return different JSX. ? : and && are expressions, so they can sit inside JSX, in {}.
The two libraries solve the same thing in their own way: React with JavaScript around the markup, Vue with attributes inside it. The result is the same: the element of a false condition is not in the DOM. It is not hidden, it does not exist.
Here status is given as a prop. How the catalog changes it by itself while loading from the API is in the module "Effects, data and forms".
See it
See which branch goes in
One value goes into the component, and only one branch goes into the DOM. Watch the "DOM" column on every change of status.
function Catalog({ status, products }) { if (status === 'loading') return <p>Loading…</p>; if (status === 'error') return <p>Error</p>; if (status === 'empty') return <p>No products</p>; return <ul>…</ul>;}
// in the DOM, but hidden<p hidden={status !== 'loading'}>Loading…</p>Step by step, in words
- status comes into the component as a prop. It is 'loading': the first if matches, and the component returns only <p>Loading…</p>. The DOM gets only it.
- status becomes 'error'. The library draws again: now the second branch matches. <p>Loading…</p> leaves the DOM, and the error message comes in.
- 'empty': only the message that the category is empty.
- 'ready': none of the if statements matches, and the component gets to the last return: the list. One branch at a time is in the DOM.
- The other way: the element is always drawn, and the hidden attribute decides whether it is seen. With status loading, hidden is false and the text is visible.
- status becomes 'ready'. The element stays in the DOM, with what is in it, and only gets display: none. Nobody sees it, and a screen reader skips it.
- A condition with if, ? : or && removes the element from the DOM. hidden only hides it. Removing is the default; hiding is for something that is toggled often or must keep what was typed.
The first part is conditional rendering: on a new value the old element leaves the DOM and the new one comes in. The second part is hiding: the element is in the DOM the whole time and is only shown or hidden. The end of the lesson is about it.
Try it
The four states
Catalog draws everything at once. Make it so that every section has only the part of its state: "Loading…" for loading, the error for error, "There are no products in this category." for empty and the list for ready. The other parts must not be on the page.
Console
Solution
Catalog.jsximport ProductCard from './ProductCard';
export default function Catalog({ status, products }) {
if (status === 'loading') return <p role="status">Loading…</p>;
if (status === 'error') return <p role="alert">The products did not load.</p>;
if (status === 'empty') return <p>There are no products in this category.</p>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
);
}
App.jsximport Catalog from './Catalog';
const PRODUCTS = [
{ id: 1, name: 'White bread', price: 1.2, stock: 5 },
{ id: 3, name: 'White cheese', price: 7.9, stock: 0 },
{ id: 2, name: 'Fresh milk', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<section aria-label="Loading">
<h2>Loading</h2>
<Catalog status="loading" products={[]} />
</section>
<section aria-label="Error">
<h2>Error</h2>
<Catalog status="error" products={[]} />
</section>
<section aria-label="Empty category">
<h2>Empty category</h2>
<Catalog status="empty" products={[]} />
</section>
<section aria-label="Ready">
<h2>Ready</h2>
<Catalog status="ready" products={PRODUCTS} />
</section>
</main>
);
}
ProductCard.jsxconst money = (x) => '€' + x.toFixed(2);
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
<button type="button" aria-label={`Add ${product.name}`}>Add</button>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
if statements with an early return for the three messages, and the list is the last return. The check looks for both the messages and the list in every section. If the list has no condition, it is also in the "Loading" section, even though it is empty: a <ul> without rows is still a list for a screen reader.Try it
Fill in
"Loading…" should show only while status is loading.
{status === 'loading' <p role="status">Loading…</p>}&&: if the left side is false, the expression is false and React draws nothing. If it is true, the expression is the element on the right.
The idea
Sold out: a badge and a disabled button
A condition is not only for whole parts. In the product card it changes a text and an attribute:
{product.stock === 0 ? ( <p className="badge">Sold out</p>) : ( <p>Only {product.stock} left</p>)}<button type="button" disabled={product.stock === 0}>Add</button>disabled={true} puts the disabled attribute on, and disabled={false} removes it.
A disabled button cannot be pressed, does not take the focus with Tab, and a screen reader says that it is unavailable. The badge says why. A button that can be pressed and then says "out of stock" is worse: the buyer finds out only after the click.
Try it
Sold out
Make ProductCard show "Sold out" and disable the "Add" button when stock is 0. Otherwise it shows "Only 5 left" and the button works. The cheese is sold out.
Console
Solution
ProductCard.jsxconst money = (x) => '€' + x.toFixed(2);
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
{product.stock === 0 ? (
<p className="badge">Sold out</p>
) : (
<p>Only {product.stock} left</p>
)}
<button type="button" aria-label={`Add ${product.name}`} disabled={product.stock === 0}>Add</button>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const PRODUCTS = [
{ id: 1, name: 'White bread', price: 1.2, stock: 5 },
{ id: 3, name: 'White cheese', price: 7.9, stock: 0 },
{ id: 2, name: 'Fresh milk', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<h1>The catalog</h1>
<ul>
{PRODUCTS.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
? : picks between the badge and "Only … left", and disabled={product.stock === 0} disables the button. The check also looks for a lone 0 on the page: {product.stock && <p>…</p>} draws 0 for the cheese. The next card is about that.The idea
The zero on the screen
&& does not return true or false. It returns the left side if that is falsy, and the right side if it is truthy. React draws numbers, 0 included, and does not draw false, null and undefined. So:
{items.length && <p>In the cart: {items.length}</p>} // empty cart: 0{items.length > 0 && <p>In the cart: {items.length}</p>} // empty cart: nothingBefore && put a comparison that gives true or false: > 0, === 0, !== null. Another way is ? : with null for "nothing".
Try it
Find the bug
With an empty cart, the top corner says "0".
Pick the line with the bug.
Line 4: items.length && … is 0 for an empty cart, and React draws 0. It has to be items.length > 0 && ….
The idea
Removed or hidden
Sometimes an element has to stay in the DOM and only not be seen. Here both panels have a field for a note. Open both, type something, close them and open them again.
Console
{removedOpen && …}: when closed it is gone, and when opened it is new, with an empty field. In the second it uses the attribute hidden={!hiddenOpen}: it stays in the DOM with display: none and keeps what was typed.Removing is the default: fewer elements on the page, nothing hidden for a screen reader. Hiding is for parts that are opened and closed often or have to keep what was entered in them.
More precisely, if you are curious
hidden and CSS. hidden works through display: none from the browser's styles. If a CSS rule gives the element display: flex, the rule wins and the element is still visible. Then you hide it with a class or with style={{ display: 'none' }}.
Same place, same element. With {isSearch ? <input placeholder="Search" /> : <input placeholder="Code" />} both branches are an <input> in the same place. React keeps the element and only changes placeholder, and what was typed stays. If you need two different elements, give them different keys.
return null. A component that should draw nothing returns null.
Class components in old code do the same in the render() method.
aria-hidden is not hiding. aria-hidden="true" hides the element only from a screen reader, and it stays on the screen. Hiding from everybody is display: none: hidden, v-show or removing.
Exercise
The cart
App shows three carts: empty, with products and with a sold out product. Make Cart so that the empty one says "The cart is empty.", with no list and no total, and the "Place order" button is disabled. The full one shows the rows, "Total: €4.00" and a working button. The sold out product has a "Sold out" badge, above the button it says "Remove the sold out products to place the order." (role alert), and the button is disabled.
Console
Solution
Cart.jsxconst money = (x) => '€' + x.toFixed(2);
export default function Cart({ items }) {
const total = items.reduce((sum, p) => sum + p.price * p.qty, 0);
const soldOut = items.some((p) => p.stock === 0);
return (
<div>
{items.length === 0 ? (
<p>The cart is empty.</p>
) : (
<>
<ul>
{items.map((p) => (
<li key={p.id}>
{p.name} × {p.qty}
{p.stock === 0 && <span className="badge">Sold out</span>}
</li>
))}
</ul>
<p>Total: {money(total)}</p>
</>
)}
{soldOut && <p role="alert">Remove the sold out products to place the order.</p>}
<button type="button" disabled={items.length === 0 || soldOut}>Place order</button>
</div>
);
}
App.jsximport Cart from './Cart';
export default function App() {
return (
<main>
<section aria-label="Empty">
<h2>Empty</h2>
<Cart items={[]} />
</section>
<section aria-label="With products">
<h2>With products</h2>
<Cart items={[
{ id: 1, name: 'White bread', price: 1.2, qty: 2, stock: 5 },
{ id: 2, name: 'Fresh milk', price: 1.6, qty: 1, stock: 2 },
]} />
</section>
<section aria-label="With a sold out product">
<h2>With a sold out product</h2>
<Cart items={[
{ id: 1, name: 'White bread', price: 1.2, qty: 1, stock: 5 },
{ id: 3, name: 'White cheese', price: 7.9, qty: 1, stock: 0 },
]} />
</section>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
items.length === 0 ? … : … picks between the message and the list with the total (the two are in <>…</>). soldOut && <p role="alert">…</p> shows the warning, and disabled={items.length === 0 || soldOut} disables the button in both cases. With items.length && … the empty cart shows 0, and the check looks for it.Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Data from an API · The first component · Props: data from above · Lists and keys
- Continues in: Updating objects and arrays · Derived state · Lifting state up · Effects
- Tasks on this topic: Components