Conditional Rendering
Ternaries, &&, and early returns - and when each one reads best.
What you will be able to do
- Explain what conditional rendering is, and why every real screen needs it
- Use if and early returns above the JSX
- Use a ternary for one of two, and && for something or nothing
- Avoid the 0 && trap, and know why || is rarely what you want
- Return null to render nothing from a component
- Handle loading, error, empty, and loaded states in the right order
- Pick the clearest technique for a given condition
The idea, in plain English
Conditional rendering means showing different UI depending on the data you have. Logged in or not, loading or loaded, admin or student, empty list or full one - almost every component you write has at least one condition.
React adds no special syntax for it. A component is a JavaScript function, so you use JavaScript: if statements and early returns above the JSX, and the ternary and && expressions inside it. The skill is choosing the one that reads best for each case.
The rule of thumb: if the whole component changes - a loading screen instead of the page - return early. If one piece changes between two options, use a ternary. If one piece is either there or not, use &&. And if a component should show nothing at all, return null.
Worked example: A loading, empty, error, and loaded state in one component.
if, above the JSX
if is a statement, so it cannot go inside the braces in JSX. It can go anywhere else in the function. Compute a value with if, store it in a variable, and render the variable: let message; if (isLoggedIn) message = "Welcome back!"; else message = "Please log in."; then return <h1>{message}</h1>.
The variable can hold JSX as well as text. That makes if the tool for choices with more than two branches - pick the element first, then drop it into the markup.
Early returns
A component can return more than once. if (!user) return <p>Loading...</p>; ends the function right there, so the JSX below it is never reached - and every line below can assume user exists.
That last part matters in TypeScript. With user typed as User | null, TypeScript knows that after the early return user is a User, so user.name compiles. Inside a big ternary you get the same narrowing, but buried in markup.
Use an early return whenever a state replaces the whole component: loading, error, not found, not allowed.
The ternary: one of two
condition ? a : b is an expression, so it fits inside braces. Both branches can be text - {isLoggedIn ? "Logout" : "Login"} - or elements - {isLoggedIn ? <Dashboard /> : <Login />}.
When a branch needs several elements, wrap them in a Fragment. When the whole return is a ternary, it can sit directly after return without an outer wrapper.
Chaining ternaries for three or more states works, but each level makes the markup harder to scan. Past two options, switch to early returns or to an if that picks a variable.
&&: something or nothing
a && b returns a when a is falsy and b otherwise. {isAdmin && <button>Admin Settings</button>} gives the button when isAdmin is true, and false when it is not - and React renders false as nothing.
Conditions can be chained: {isLoggedIn && isOwner && <button>Edit Course</button>} shows the button only when both are true.
The left side must be a real boolean. With lessonsCompleted = 0, {lessonsCompleted && <p>Course started</p>} returns 0, and React prints 0 on the page. Write {lessonsCompleted > 0 && ...} instead - Lesson 6 covers why.
Watch out: Counts and lengths are the usual culprits. {items.length && <List />} prints 0 for an empty list; {items.length > 0 && <List />} prints nothing.
Why || is rarely what you want
a || b returns a when a is truthy and b otherwise - the reverse of &&. So {isAdmin || <AdminPanel />} shows nothing to admins, because true renders as nothing, and shows the panel to everyone else. It is almost never what you meant.
|| and ?? have a real use in JSX: supplying a fallback value, not deciding whether to render. {user.nickname || user.name} shows the nickname if there is one. ?? is stricter - it only falls back on null and undefined, so {score ?? "Not attempted"} still shows a score of 0.
return null: render nothing
A component can return null to render nothing at all. function AdminButton({ isAdmin }) { if (!isAdmin) return null; return <button>Admin Settings</button>; } lets the component own its own visibility rule.
The parent then writes <AdminButton isAdmin={user.isAdmin} /> without a condition around it. Whether that is cleaner than {isAdmin && <AdminButton />} in the parent depends on whether the rule belongs to the button or to the page.
Loading, error, empty, loaded
Any component that shows fetched data has four states: still loading, failed, succeeded with nothing to show, and succeeded with data. Early returns handle them in order, one per if.
The order matters. Check loading first, because there is no data or error yet. Check error next, because a failed request has no data. Then check for empty, and only then render the real UI - which can now assume the data exists and is not empty.
You will write this shape again and again: with useEffect, with TanStack Query, with Redux, on dashboards and detail pages. Learn it once here.
Choosing a technique
Early return when a state replaces the whole component. Ternary when one piece is one of two things. && when one piece is either there or not, with a boolean on the left. if into a variable when there are three or more options. return null when a component should hide itself.
Conditions do not only decide what exists. The same expressions choose attribute values - disabled={isSaving}, className={completed ? "lesson completed" : "lesson"} - and text - {isSaving ? "Saving..." : "Save"}.
Whole component changesEarly return: if (loading) return <Loading />;A or BTernary: {isLoggedIn ? <Dashboard /> : <Login />}Something or nothing&&: {isAdmin && <AdminPanel />} - boolean on the leftThree or more optionsif into a variable, or one early return per optionComponent hides itselfreturn null;Fallback value?? or ||: {user.nickname ?? user.name}Syntax and examples
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
let message;
if (isLoggedIn) {
message = "Welcome back!";
} else {
message = "Please log in.";
}
return <h1>{message}</h1>;
}type User = { name: string; email: string };
function Profile({ user }: { user: User | null }) {
if (!user) {
return <p>Loading...</p>;
}
// From here on, TypeScript knows user is a User, not null.
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}function Profile({ user }: { user: User | null }) {
return (
<div>
{!user ? (
<p>Loading...</p>
) : (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)}
</div>
);
}function LoginButton({ isLoggedIn }: { isLoggedIn: boolean }) {
return <button>{isLoggedIn ? "Logout" : "Login"}</button>;
}
function CourseStatus({ completed }: { completed: boolean }) {
return completed ? (
<>
<h2>Course Completed</h2>
<p>Congratulations!</p>
</>
) : (
<>
<h2>Course In Progress</h2>
<p>Keep learning.</p>
</>
);
}function CourseActions({
isLoggedIn,
isOwner,
}: {
isLoggedIn: boolean;
isOwner: boolean;
}) {
return (
<div>
{isLoggedIn && <button>Enroll</button>}
{isLoggedIn && isOwner && <button>Edit Course</button>}
</div>
);
}{true && <p>Panel</p>} // <p>Panel</p>
{false && <p>Panel</p>} // nothing
{true || <p>Panel</p>} // nothing - true renders as nothing
{false || <p>Panel</p>} // <p>Panel</p> - the reverse of what you wanted
{0 && <p>Started</p>} // 0 - printed on the page
{0 > 0 && <p>Started</p>} // nothingfunction AdminButton({ isAdmin }: { isAdmin: boolean }) {
if (!isAdmin) {
return null;
}
return <button>Admin Settings</button>;
}
// The parent needs no condition:
<AdminButton isAdmin={user.isAdmin} />type Role = "student" | "trainer" | "admin";
function Dashboard({ role }: { role: Role }) {
let content;
if (role === "student") {
content = <StudentDashboard />;
} else if (role === "trainer") {
content = <TrainerDashboard />;
} else {
content = <AdminDashboard />;
}
return (
<main>
<h1>Dashboard</h1>
{content}
</main>
);
}type Course = { id: number; title: string };
type CourseListProps = {
loading: boolean;
error: string | null;
courses: Course[];
};
function CourseList({ loading, error, courses }: CourseListProps) {
if (loading) {
return <p>Loading courses...</p>;
}
if (error) {
return <p role="alert">{error}</p>;
}
if (courses.length === 0) {
return <p>No courses available.</p>;
}
return (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}// Works, but nobody can read it
return (
<div>
{loading ? <Loading /> : error ? <ErrorMessage /> : data ? (data.length ? <List data={data} /> : <Empty />) : null}
</div>
);
// The same logic, one state per line
if (loading) return <Loading />;
if (error) return <ErrorMessage />;
if (!data || data.length === 0) return <Empty />;
return <List data={data} />;function SaveButton({ saving }: { saving: boolean }) {
return (
<button disabled={saving} className={saving ? "btn btn-busy" : "btn"}>
{saving ? "Saving..." : "Save"}
</button>
);
}
function ProgressText({ completed }: { completed: number }) {
return (
<p>
{completed === 0
? "Start your course"
: `${completed} ${completed === 1 ? "lesson" : "lessons"} completed`}
</p>
);
}
// 0 -> Start your course 1 -> 1 lesson completed 5 -> 5 lessons completedTip: Read your early returns top to bottom as a list of reasons not to show the page yet. Whatever survives to the last return is the normal case - and can assume everything the checks ruled out.
Conditional rendering at a glance
if / elseOutside JSX. Picks a value or an element into a variable.
let content; if (a) content = <A />;
Early returnEnds the component for a special state.
if (loading) return <Loading />;
TernaryInside JSX. One of two.
{ok ? <A /> : <B />}&&Inside JSX. Something or nothing.
{count > 0 && <Badge />}return nullThe component renders nothing.
if (!allowed) return null;
?? / ||A fallback value, not a render decision.
{nickname ?? name}Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Render CourseList four times: loading true; error "Network error"; courses []; and two courses. Then swap the order of the loading and error checks and see what breaks.”
“Write {lessonsCompleted && <p>Course started</p>} with lessonsCompleted = 0, find the stray 0 on the page, and fix it.”
“Change {isAdmin && <AdminPanel />} to ||, and render it with isAdmin true and then false. Who sees the panel now?”
“In Profile, delete the early return. What does TypeScript say about user.name, and why?”
What usually goes wrong
if is a statement and braces need an expression. Use && or a ternary inside the JSX, or move the if above the return.
✗ <div>
{if (isAdmin) { <Admin /> }}
</div>✓ <div>
{isAdmin && <Admin />}
</div>When it is 0, the expression returns 0, and React prints it.
✗ {count && <Badge />}✓ {count > 0 && <Badge />}|| returns the left side when it is truthy, so the element appears only when the condition is false.
✗ {isAdmin || <AdminPanel />}✓ {isAdmin && <AdminPanel />}Each level makes the markup harder to scan and to change.
✗ {a ? <A /> : b ? <B /> : c ? <C /> : <D />}✓ if (a) return <A />;
if (b) return <B />;
if (c) return <C />;
return <D />;While a request is loading there is no data yet, so an empty check placed first shows "No courses" for a moment before the real list.
✗ if (courses.length === 0) return <Empty />;
if (loading) return <Loading />;✓ if (loading) return <Loading />;
if (error) return <ErrorMessage />;
if (courses.length === 0) return <Empty />;const user = null; makes TypeScript treat user as always null, so user.name fails with "Property name does not exist on type never". Type the value as it will really be.
✗ const user = null;✓ const user: User | null = null;Best practices
- Handle special states with early returns, in the order loading, error, empty.
- Use a ternary for two options and && for one; move three or more out of the JSX.
- Keep the left side of && a real boolean.
- Type values that can be missing as T | null, and let early returns narrow them.
- Give every fetched list an empty state - a blank area looks like a bug.
- Decide whether a visibility rule belongs to the component (return null) or its parent (&&).
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Write a CoursePage that takes loading, error, and courses props, and shows "Loading courses...", "Failed to load courses.", "No courses available.", or the list of course titles.
Show hintHide hint
Three early returns, then the list.
Show solutionHide solution
type Course = { id: number; title: string };
function CoursePage({
loading,
error,
courses,
}: {
loading: boolean;
error: boolean;
courses: Course[];
}) {
if (loading) {
return <p>Loading courses...</p>;
}
if (error) {
return <p>Failed to load courses.</p>;
}
if (courses.length === 0) {
return <p>No courses available.</p>;
}
return (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}For each, choose &&, a ternary, an early return, or return null: (a) a "New" badge on recent lessons, (b) a Login or Logout button, (c) a spinner while the whole page loads, (d) a PremiumBanner that hides itself for premium users.
Show hintHide hint
Ask: something or nothing, one of two, the whole component, or the component deciding for itself?
Show solutionHide solution
(a) && {isNew && <Badge>New</Badge>}
(b) ternary {isLoggedIn ? <LogoutButton /> : <LoginButton />}
(c) early return if (loading) return <Spinner />;
(d) return null inside PremiumBanner: if (isPremium) return null;Fix this component so it never shows a stray 0 and shows "1 lesson" rather than "1 lessons": {count && <p>{count} lessons completed</p>}.
Show hintHide hint
Two separate conditions - one for whether to render, one for the word.
Show solutionHide solution
function Completed({ count }: { count: number }) {
return (
<div>
{count > 0 && (
<p>
{count} {count === 1 ? "lesson" : "lessons"} completed
</p>
)}
</div>
);
}A lesson status control
Render a lesson status as a locked label, a start button, or a completed label - with no nested ternaries.
- Accept status: "locked" | "available" | "completed".
- locked shows a span with "🔒 Lesson Locked".
- available shows a button with "▶ Start Lesson".
- completed shows a span with "✓ Completed".
- Use early returns, one per status.
- Render a list of three lessons, one in each status.
type LessonStatusProps = {
status: "locked" | "available" | "completed";
};
function LessonStatus({ status }: LessonStatusProps) {
// one early return per status
}Show one solutionHide solution
type Status = "locked" | "available" | "completed";
function LessonStatus({ status }: { status: Status }) {
if (status === "locked") {
return <span>🔒 Lesson Locked</span>;
}
if (status === "available") {
return <button>▶ Start Lesson</button>;
}
return <span>✓ Completed</span>;
}
const lessons: { id: number; title: string; status: Status }[] = [
{ id: 1, title: "JSX Fundamentals", status: "completed" },
{ id: 2, title: "Conditional Rendering", status: "available" },
{ id: 3, title: "Rendering Lists", status: "locked" },
];
function LessonList() {
return (
<ul>
{lessons.map((lesson) => (
<li key={lesson.id}>
{lesson.title} <LessonStatus status={lesson.status} />
</li>
))}
</ul>
);
}Key points
- Conditional rendering is plain JavaScript - no special React syntax.
- if belongs above the JSX; inside braces, use a ternary or &&.
- Early returns handle states that replace the whole component.
- A ternary picks one of two; && picks something or nothing.
- Keep the left side of && a boolean, or a 0 appears on the page.
- || shows its right side only when the left is falsy - rarely what you want for rendering.
- return null renders nothing.
- Fetched data: check loading, then error, then empty, then render.
Quick check before you move on
Interview questions
What is conditional rendering?
Showing different UI depending on data or state. React has no special syntax for it - you use JavaScript: if and early returns outside the JSX, ternaries and && inside it.
When do you use &&, a ternary, or an early return?
&& for something or nothing, with a boolean on the left. A ternary for one of two. An early return when a state - loading, error, not found - replaces the whole component.
Why can {count && <Badge />} be a bug?
When count is 0, && returns 0, and React renders numbers, so a stray 0 appears. Compare explicitly: {count > 0 && <Badge />}.
How do you render nothing from a component?
Return null. The component still exists in the tree, but produces no DOM.
How do you structure loading, error, and success states?
Early returns in order: loading first, then error, then empty, then the real UI. Each check rules something out, so the final return can assume the data exists.
Why avoid nested ternaries?
They work, but each level makes the markup harder to read and to change. Early returns or an if that picks a variable express the same logic one case per line.
Quiz
- 1.
Which shows an Admin button only to admins? A. {isAdmin && <AdminButton />} B. {if (isAdmin) <AdminButton />} C. {isAdmin || <AdminButton />} D. {<AdminButton /> && isAdmin}
- 2.
Which fits two different UIs? A. condition && <A /> B. condition ? <A /> : <B />
- 3.
count is 0. What does {count && <Badge />} render? A. Nothing B. The badge C. 0 D. An error
- 4.
Which is clearest for a loading state that replaces the whole component? A. if (loading) return <Loading />; B. A nested ternary around all the JSX
- 5.
What does return null; do in a component? A. Throws an error B. Renders the text null C. Renders nothing D. Unmounts the parent
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned