Home / Articles / Handling Real-World UI States with React Conditional Rendering

This article is published in English.

Handling Real-World UI States with React Conditional Rendering

Learn how to build authentication, role, permission, loading, error, and empty-state UIs in React using practical conditional rendering patterns.

2186 words

In the previous installment, you covered the basics of conditional rendering — using simple conditions to tell React whether to show one component, another component, or nothing at all.

Real-world applications, however, rarely stay this simple:

isLoggedIn ? <Dashboard /> : <Login />

Consider a real dashboard. A user might be in any of these states:

  • Logged out
  • In the process of logging in
  • Logged in
  • An Admin
  • A regular User
  • Missing a required permission
  • Waiting for data to arrive
  • Dealing with an API failure
  • Viewing an empty result set

Each of these scenarios calls for a distinct UI. This is exactly where conditional rendering stops being a small trick and becomes a genuine tool for structuring your application.

Let's look at how this plays out in production-grade React code.

1. Authentication-Based Rendering

A classic use case is authentication. Picture an app with two possible screens:

Not Logged In
      ↓
Login Page

Logged In
      ↓
Dashboard

React can pick between them without much effort:

function App() {
  const isLoggedIn = true;
return (
    <>
      {isLoggedIn ? <Dashboard /> : <Login />}
    </>
  );
}

Real apps, though, usually need a third state: loading, since the app may take a moment to confirm whether the user's token is valid. The flow then looks like this:

Checking Authentication
        ↓
      Loading
        ↓
  Authenticated?
   ↙          ↘
 YES          NO
 ↓             ↓
Dashboard     Login

In code, that might translate to:

function App() {
  const isLoading = false;
  const isLoggedIn = true;
if (isLoading) {
    return <LoadingSpinner />;
  }
  return isLoggedIn
    ? <Dashboard />
    : <Login />;
}

You'll see this exact pattern repeated across countless production React apps.

2. Role-Based Rendering

Authentication tells you who is logged in. Authorization tells you what that person is allowed to do.

Take an employee management tool as an example. An Admin might see:

View Employees
Add Employee
Edit Employee
Delete Employee

While a standard User only sees:

View Employees

You can conditionally show actions depending on the user's role:

function EmployeeCard({ userRole }) {
  return (
    <div>
      <h2>Employee Details</h2>
      <button>View</button>
      {userRole === "admin" && (
        <>
          <button>Edit</button>
          <button>Delete</button>
        </>
      )}
    </div>
  );
}

With this setup, admin-only controls only appear for admins.

Important Security Note

Conditional rendering controls what shows up in the UI, but hiding an element from view is not a substitute for real security. For instance:

{isAdmin && <DeleteButton />}

This keeps a regular user from seeing the button, but the server still has to independently confirm that the request actually comes from someone authorized to delete data. Think of the split this way:

Frontend
↓
Controls what users SEE

Backend
↓
Controls what users CAN DO

Never treat client-side conditional rendering as your authorization mechanism.

3. Permission-Based UI

Larger applications often need more granularity than simple roles like:

Admin
User

Instead, you might define specific permissions such as:

CAN_VIEW_USERS
CAN_EDIT_USERS
CAN_DELETE_USERS
CAN_EXPORT_REPORT

Components can then render each action individually based on the permissions available:

function UserActions({ permissions }) {
  return (
    <>
      {permissions.includes("CAN_EDIT_USERS") && (
        <button>Edit</button>
      )}
      {permissions.includes("CAN_DELETE_USERS") && (
        <button>Delete</button>
      )}
    </>
  );
}

This approach gives you much more precise control over what each user can do.

4. Loading States

Suppose a dashboard fires off an API call that takes two seconds to resolve. What should appear on screen in that gap? Certainly not a blank page — you want a loading indicator instead:

if (loading) {
  return <p>Loading products...</p>;
}

The overall flow looks like this:

API Request
    ↓
Loading = true
    ↓
Show Loader
    ↓
API Response
    ↓
Loading = false
    ↓
Show Content

Loading indicators make an app feel responsive even when the network is slow.

5. Skeleton Loaders

Rather than a plain text message like:

Loading...

many modern interfaces show a placeholder shaped like the content that's about to load — a skeleton loader. For example:

┌──────────────────────┐
│ █████████████        │
│ ███████              │
│ █████████████████    │
└──────────────────────┘

Once the real data comes back, it replaces the placeholder:

┌──────────────────────┐
│ MacBook Air          │
│ ₹99,999              │
│ ⭐⭐⭐⭐⭐             │
└──────────────────────┘

The underlying React logic stays just as simple:

return loading
  ? <ProductSkeleton />
  : <ProductCard />;

The only real difference here is the polish it adds to the user experience.

6. Error States

Things don't always go smoothly with API calls.

The connection can drop.

The server can crash.

A request can time out.

Rather than letting your app crash or freeze, you should render an error state instead.

if (error) {
  return (
    <div>
      <h2>Something went wrong.</h2>
      <button>Try Again</button>
    </div>
  );
}

Handling failures gracefully is a hallmark of production-quality UI.

7. Loading + Error + Success

In practice, these three states almost always show up together.

function ProductList({
  loading,
  error,
  products
}) {
      if (loading) {
    return <p>Loading...</p>;
      }
  if (error) {
    return <p>Something went wrong.</p>;
      }
  return <Products products={products} />;
     }

You can picture the flow like this:

Request
   │
   ├── Loading → Loader
   │
   ├── Failed → Error
   │
   └── Success → Data

Once you get to API calls and useEffect later in this series, you'll see this exact pattern come up again and again.

8. Empty States

Just because a request succeeds doesn't guarantee there's actual data to show.

Say a user searches for something like:

"React Quantum Pizza Developer"

The API call itself completes without any errors.

But the result might look like:

products.length === 0

Rather than leaving the screen blank, give the user something meaningful to see.

if (products.length === 0) {
  return (
    <div>
      <h2>No Products Found</h2>
      <p>Try changing your search.</p>
    </div>
  );
}

Empty states matter a lot for a polished user experience.

Loading vs Empty vs Error

New React developers often mix these three up, but they represent very different situations.

LOADING
Data hasn't arrived yet.

EMPTY
Data arrived, but nothing exists.

ERROR
Something failed.

A solid application accounts for all three separately.

9. Multiple Conditions

Sometimes what you render depends on more than one condition stacked together.

Consider this flow:

Is User Logged In?
        ↓
Is Subscription Active?
        ↓
Is User Admin?
        ↓
Show Admin Dashboard

It's tempting to squeeze all of this into one giant nested ternary:

condition1
  ? condition2
    ? condition3
      ? <A />
      : <B />
    : <C />
  : <D />

It compiles fine.

But it's a nightmare to read.

A better approach is to break each condition out clearly.

if (!isLoggedIn) {
  return <Login />;
}

if (!hasSubscription) {
  return <UpgradePlan />;
}

if (isAdmin) {
  return <AdminDashboard />;
}

return <UserDashboard />;

This version is far easier to follow.

10. Guard Clauses

The pattern you just saw has a name: guard clauses, also known as early returns.

Instead of nesting condition inside condition inside condition:

if
 └── if
      └── if
           └── UI

deal with the edge cases up front and return early.

if (loading) return <Loader />;
if (error) return <ErrorPage />;
if (!user) return <Login />;
return <Dashboard />;

The result is clean.

It's readable.

And it's much easier to debug.

Think Like a React Developer

Before writing a component, it helps to ask yourself:

"What are all the possible states this screen could be in?"

For a page driven by an API call, that list might look like:

Loading
Error
Empty
Success

For an authentication flow, it might look like:

Logged Out
Checking Authentication
Logged In
Unauthorized

Mapping out these states before you start coding makes the resulting component far easier to reason about.

Common Beginner Mistakes

Piling Up Nested Ternaries

Don't trade away readability just to save a few lines of code.

Skipping the Empty State

An API returning an empty array isn't the same as an error — treat it as its own case.

Mixing Up UI Hiding with Real Security

Hiding something like:

<DeleteButton />

does nothing to stop someone from hitting your API endpoint directly.

Real authorization has to live on the backend.

Letting && Render the Wrong Thing

Watch out for code like:

{items.length && <ProductList />}

If items.length happens to be 0, React can end up rendering:

0

right there on the page.

The safer version is:

{items.length > 0 && <ProductList />}

Now the condition evaluates to an actual boolean.

Best Practices

Readability should always come first in conditional rendering.

Favor patterns like:

if (loading) return <Loader />;

instead of piling conditions up inside deeply nested JSX.

For states you render often, pull them into their own reusable components:

<Loader />
<ErrorMessage />
<EmptyState />

As components grow more complex, separate the business logic from what's actually being displayed.

And don't just design for the happy path — plan for every state the UI could realistically be in.

Mini Project: Smart Dashboard

As practice, try building a dashboard that accounts for cases like:

User Not Logged In
        ↓
Login ScreenUser

    Logged In
        ↓
Loading Dashboard
        ↓
 ┌──────┴──────┐
Error          Success
 ↓                ↓
Error UI       Data Exists?
               ↙       ↘
             YES        NO
              ↓          ↓
          Dashboard   Empty State

Then layer in role-based behavior on top:

Admin
↓
Edit + Delete

User
↓
View Only

A project like this pulls together several concepts at once:

  • Props
  • State
  • Events
  • Conditional Rendering

This is exactly how the individual pieces of React start clicking together once you build something real.

Interview Questions

What is conditional rendering?

It's the practice of showing different UI depending on the current state or conditions in your application.

What's the difference between && and a ternary?

Use && when you only want something to render in the true case, with nothing shown otherwise. Use a ternary when you need distinct output for both the true and false cases.

What counts as an empty state?

It's the UI you show when a request succeeds but there's simply no data to display.

Is role-based rendering on the frontend enough for security?

No. It's a UI convenience — actual authorization checks still need to happen on the backend.

What's the benefit of early returns?

They cut down on nesting, which keeps conditional components easier to read and maintain.

Key Takeaways

At this point, you've covered the full scope of conditional rendering in React.

You've seen how real-world apps deal with signed-in versus signed-out views, restricting screens by role, gating features by fine-grained permission, showing spinners while data loads, using skeleton placeholders instead of plain text, surfacing errors when requests fail, messaging users when a result set is empty, combining several conditions into one coherent flow, simplifying nested checks with guard clauses, and applying the broader habits that keep this logic maintainable in a real codebase.

Conditional rendering is what lets your app show the right experience for whatever situation it's currently in.

But there's still a challenge ahead.

Suppose an API hands back:

1,000 Products

Would you really write out:

<Product />
<Product />
<Product />
...

one thousand separate times?

Obviously not.

React has a much better way to handle this.

In Part 9A, you'll learn how to render lists using map(), and see how a single component can generate hundreds or thousands of UI elements straight from your data.

Right after that, you'll tackle one of React's most classic interview questions:

Why does React require a key?

See you in Part 9A — Rendering Lists & Keys in React.