Declarative conditional rendering for React. Replace nested ternaries and && chains with a readable <Match> / <Match.When> / <Match.Default> component: a switch / if-else if-else for JSX.
npm install guard-condition
# or
pnpm add guard-condition
# or
yarn add guard-condition- Tiny: a single file, zero dependencies
- Written in TypeScript, ships with type definitions
- Works with React 16.8+ (hooks era) through React 19
- First match wins, just like
if / else if / else - Works with any UI library (Ant Design, MUI, Chakra, Tailwind, plain HTML)
Conditional rendering in React gets messy fast. A loading / empty / no-results / list screen usually ends up as a nested ternary:
{isLoading ? (
<Spin />
) : goals.length === 0 && !filters?.search?.trim() ? (
<EmptyState title="No goals added" />
) : goals.length === 0 ? (
<EmptyState title="No goals found" />
) : (
goals.map((goal) => <GoalItem key={goal.id} goal={goal} />)
)}Nested ternaries are hard to read, hard to diff, and easy to break. With guard-condition, the same logic reads top to bottom:
import Match from "guard-condition";
<Match>
<Match.When condition={isLoading}>
<Spin />
</Match.When>
<Match.When condition={goals.length === 0 && !filters?.search?.trim()}>
<EmptyState title="No goals added" />
</Match.When>
<Match.When condition={goals.length === 0}>
<EmptyState title="No goals found" />
</Match.When>
<Match.Default>
{goals.map((goal) => (
<GoalItem key={goal.id} goal={goal} />
))}
</Match.Default>
</Match>import Match from "guard-condition";
function Status({ status }: { status: "idle" | "loading" | "error" | "success" }) {
return (
<Match>
<Match.When condition={status === "loading"}>
<p>Loading…</p>
</Match.When>
<Match.When condition={status === "error"}>
<p>Something went wrong.</p>
</Match.When>
<Match.When condition={status === "success"}>
<p>Done!</p>
</Match.When>
<Match.Default>
<p>Nothing to show yet.</p>
</Match.Default>
</Match>
);
}Container. Looks at its direct children in order and renders the content of the first <Match.When> whose condition is truthy. If none match, it renders <Match.Default> (if present), otherwise null.
| Prop | Type | Description |
|---|---|---|
condition |
boolean |
Renders children if this is the first match |
children |
ReactNode |
Content to render |
Fallback content, like else or default:. Place it last: Match returns as soon as it reaches a Default, so any When placed after it is never evaluated.
The most common case for data-fetching UIs (React Query, SWR, Apollo, RTK Query):
<Match>
<Match.When condition={isLoading}><Skeleton /></Match.When>
<Match.When condition={isError}><ErrorMessage error={error} /></Match.When>
<Match.When condition={data?.length === 0}><EmptyState /></Match.When>
<Match.Default><DataTable rows={data} /></Match.Default>
</Match><Match>
<Match.When condition={user.role === "admin"}><AdminPanel /></Match.When>
<Match.When condition={user.role === "editor"}><EditorTools /></Match.When>
<Match.Default><ReadOnlyView /></Match.Default>
</Match><Match>
<Match.When condition={!isAuthReady}><Spinner /></Match.When>
<Match.When condition={!isLoggedIn}><LoginForm /></Match.When>
<Match.Default><Dashboard /></Match.Default>
</Match><Match>
<Match.When condition={step === 1}><AccountStep /></Match.When>
<Match.When condition={step === 2}><ProfileStep /></Match.When>
<Match.When condition={step === 3}><ReviewStep /></Match.When>
</Match><Match>
<Match.When condition={flags.newCheckout}><NewCheckout /></Match.When>
<Match.Default><LegacyCheckout /></Match.Default>
</Match><Match>
<Match.When condition={isMobile}><MobileNav /></Match.When>
<Match.When condition={isTablet}><TabletNav /></Match.When>
<Match.Default><DesktopNav /></Match.Default>
</Match>| Nested ternary | && chains |
guard-condition | |
|---|---|---|---|
| Readable with 3+ branches | ❌ | ❌ | ✅ |
| Explicit "else" | ✅ | ❌ | ✅ |
| Mutually exclusive branches | ✅ | ❌ (each && is independent) |
✅ |
| Clean diffs when adding a branch | ❌ | ✅ | |
Risk of rendering 0 / "" by accident |
– | count && <X/>) |
✅ (condition is a boolean) |
- Direct children only.
Matchinspects its direct children, soMatch.When/Match.Defaultmust be placed directly inside<Match>. Wrapping them in your own component or a fragment means they will not be detected. - Order matters. The first truthy
Whenwins. Put more specific conditions first andDefaultlast. - Branches are not lazy. JSX inside every branch is created (not rendered or mounted) each time
Matchrenders, so expressions like{items.map(...)}in a non-matching branch still run. Only the matching branch's components are mounted. Guard any expression that could throw (for example, usedata?.items). - No match, no output. Without a matching
Whenor aDefault,Matchrendersnull.
Types are included. condition is typed as boolean, so coerce values explicitly:
<Match.When condition={Boolean(user)}>…</Match.When>
<Match.When condition={items.length > 0}>…</Match.When>How do I do if / else in React JSX without ternaries?
Use <Match.When> for the if and <Match.Default> for the else.
Does it support switch statements?
Yes: use one Match.When per case, e.g. condition={status === "error"}, plus Match.Default for default:.
Does it work with server components / Next.js?
It uses only React.Children and plain elements (no hooks, state, or effects), so it can be used in both server and client components.
Does it work with React 16, 17, 18, and 19?
Yes, the peer dependency is react >= 16.8.0.
MIT © Mutasim Al-Mualimi