Nader Ikladious. N.
3 min read

Rethinking Conditional Rendering in React

A deep dive into the architecture behind React Flow — an exploration of cleaner patterns for conditional rendering that scale with complexity.

react architecture typescript open-source

React gives you incredible power to compose UIs. But there’s one area where even experienced teams struggle: conditional rendering at scale. As components grow, ternary expressions and && chains become harder to read, harder to maintain, and harder to reason about.

React Flow is my attempt to fix this.

The Problem with Inline Conditionals

Here’s a pattern every React developer recognizes:

function Dashboard({ user, isLoading, error, data }) {
if (isLoading) return <Skeleton />;
if (error) return <ErrorState message={error.message} />;
if (!user) return <LoginPrompt />;
return (
<div>
<Header user={user} />
{data.length > 0 ? (
<DataGrid items={data} />
) : (
<EmptyState message="No data yet" />
)}
{user.isAdmin && <AdminPanel />}
{user.plan === "pro" ? (
<ProFeatures />
) : user.plan === "team" ? (
<TeamFeatures />
) : (
<UpgradePrompt />
)}
</div>
);
}

This works, but it doesn’t scan. The branching logic is tangled with the rendering logic. When you need to add a new condition or change the priority of checks, you’re editing deeply nested JSX.

A Declarative Alternative

What if conditional rendering looked more like routing? Clear, declarative, and compositional:

import { Show, Match, Switch, When } from "react-flow";
function Dashboard({ user, isLoading, error, data }) {
return (
<Switch>
<Match when={isLoading}>
<Skeleton />
</Match>
<Match when={error}>
<ErrorState message={error.message} />
</Match>
<Match when={!user}>
<LoginPrompt />
</Match>
<Match when={user}>
<div>
<Header user={user} />
<Show
when={data.length > 0}
fallback={<EmptyState message="No data yet" />}
>
<DataGrid items={data} />
</Show>
<When condition={user.isAdmin}>
<AdminPanel />
</When>
</div>
</Match>
</Switch>
);
}

Each branch is explicit. The reading order matches the priority order. Adding or removing conditions is a matter of adding or removing <Match> blocks.

Architecture Decisions

Type Safety First

Every component in React Flow is fully typed. The when prop carries its type through to children via render props:

<Show when={user} fallback={<LoginPrompt />}>
{(resolvedUser) => <Profile user={resolvedUser} />}
</Show>

Here, resolvedUser is correctly typed as non-nullable — the Show component guarantees it.

Zero Runtime Cost

React Flow compiles to standard conditional checks. There’s no runtime overhead, no context providers, no virtual DOM wrappers. The components are purely organizational — they help humans read the code without adding cost for the machine.

Composability

Flow components compose naturally with the rest of React:

<Suspense fallback={<Skeleton />}>
<Show when={data}>
{(items) => (
<ErrorBoundary fallback={<ErrorState />}>
<DataGrid items={items} />
</ErrorBoundary>
)}
</Show>
</Suspense>

Lessons from Building Developer Tools

Building React Flow taught me several things about developer tooling:

  1. Don’t fight the platform. React Flow works with React’s model, not against it. It doesn’t introduce new paradigms — it wraps existing ones in clearer abstractions.

  2. Naming is the hardest part. I went through dozens of iterations on component names. Show vs If, Match vs Case, Switch vs Select. The final names needed to be immediately intuitive to someone who’d never seen the library.

  3. Documentation is the product. For a library this small, the README is the user experience. I spent more time on docs than on implementation.

  4. Constraints breed creativity. Limiting the API surface to five components forced me to make each one do exactly the right thing, with no overlap and no gaps.

What’s Next

React Flow is still evolving. I’m exploring:

  • Exhaustive matching — compile-time checks that all cases are handled
  • Async conditions — first-class support for promise-based conditions
  • DevTools integration — visualizing the active branch in React DevTools

The goal isn’t to replace React’s built-in conditional patterns — it’s to provide a better default for teams that value readability and maintainability at scale.