What renders and what does not
{"text"} // the text
{42} // the number
{0} // ZERO — this one catches everyone
{elements} // elements and arrays of them
{null} // nothing
{undefined} // nothing
{false} // nothing
{true} // nothing
{[]} // nothing
{items.length && <List />} renders 0 when the array is empty. {items.length > 0 && <List />} renders nothing. Always make the left side a boolean.
The three forms
Nested ternaries past one level become unreadable fast. Assigning to a variable above the return costs three lines and stays readable at five outcomes.
// show or nothing
{error && <p className="error">{error}</p>}
// one or the other
{isOpen ? <Panel /> : <Placeholder />}
// several outcomes: decide above the return
let body;
if (status === "loading") body = <Spinner />;
else if (status === "error") body = <Error message={message} />;
else if (items.length === 0) body = <Empty />;
else body = <List items={items} />;
return <section>{body}</section>;
A lookup for many cases
const VIEWS = {
loading: <Spinner />,
empty: <Empty />,
error: <ErrorPanel />,
};
return VIEWS[status] ?? <List items={items} />;
Every screen has four states
Any interface that loads something can be in four states, and the one that gets forgotten is the third:
- Loading — the request is in flight.
- Ready — there is data, show it.
- Empty — the request succeeded and there is nothing. This is not an error and must not look like one.
- Error — the request failed, with a message and a way to try again.
function Doors({ status, doors, error, onRetry }) {
if (status === "loading") return <p>Loading…</p>;
if (status === "error") {
return (
<div role="alert">
<p>{error}</p>
<button onClick={onRetry}>Try again</button>
</div>
);
}
if (doors.length === 0) return <p>No doors yet. Add the first one.</p>;
return <ul>{doors.map((d) => <li key={d.id}>{d.name}</li>)}</ul>;
}
Early returns keep each state on its own line of reasoning. An empty state that says what to do next is worth more than one that says "No results".
Hiding with CSS, or not rendering
If the panel holds a half-typed form you do not want to lose, keep it mounted and hide it. If it is expensive and starts fresh each time, do not render it.
// not rendered: no DOM, no cost, and state inside it is LOST
{isOpen && <Panel />}
// rendered and hidden: keeps its DOM and its state, costs the render
<Panel hidden={!isOpen} />
<div style={{ display: isOpen ? "block" : "none" }}><Panel /></div>
Guarding against missing data
function Profile({ user }) {
if (!user) return null; // the honest guard
return <p>{user.name}</p>;
}
// or handle it where the data lives
{user ? <Profile user={user} /> : <Skeleton />}
// and optional chaining for one level
<p>{user?.address?.city ?? "Unknown"}</p>
Returning nothing
return null is a valid, ordinary thing for a component to do. It is not an error and it is not a placeholder — it means "there is nothing to show here", which is exactly what a conditional row, an empty toast or a hidden tooltip needs.