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 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:
-
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.
-
Naming is the hardest part. I went through dozens of iterations on component names.
ShowvsIf,MatchvsCase,SwitchvsSelect. The final names needed to be immediately intuitive to someone who’d never seen the library. -
Documentation is the product. For a library this small, the README is the user experience. I spent more time on docs than on implementation.
-
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.