switch(true): The Stylish Way to Simplify Complex Conditionals in React
Most React components don’t render one thing; they render one of several things based on a mess of state. switch(true) is a small trick that makes those branches read like a decision table instead of a maze.
The pattern is simple: instead of switching on a value, you switch on the boolean true and let each case evaluate its own expression.
switch (true) {
case condition1:
return <A />;
case condition2:
return <B />;
default:
return <C />;
}
The first matching case wins. That ordering matters, and it’s the whole reason this pattern is useful.
When to reach for it
Reach for switch(true) when:
- You’re branching on multiple variables (not a single enum), and
- The branches have a clear precedence order (“admin beats active beats default”), and
- You’d otherwise write a 4+ branch
if-elseladder.
For a single enum value, a plain switch (status) or a lookup object is cleaner. Don’t replace the wrong tool.
Use case 1: rendering by request state
The classic loading/error/empty/data flow is where if-else ladders go to die. Order matters: error before empty, empty before data.
function UserList({ query }: { query: UseQueryResult<User[]> }) {
switch (true) {
case query.isLoading:
return <Spinner />;
case query.isError:
return <ErrorState error={query.error} onRetry={query.refetch} />;
case query.data?.length === 0:
return <EmptyState message="No users yet" />;
default:
return <Table rows={query.data!} />;
}
}
The non-null assertion on query.data! is safe because the earlier cases already excluded loading, error, and empty states. Reading top to bottom tells you exactly what the component does in each state.
Use case 2: role-and-state authorization
Anywhere you have a role plus a status (or a feature flag plus a permission), switch(true) flattens nested ternaries:
function ProjectActions({ user, project }: Props) {
switch (true) {
case project.archived:
return <ReadOnlyBanner />;
case user.role === "owner":
return <OwnerActions project={project} />;
case user.role === "editor" && project.lockedBy === user.id:
return <EditorActions project={project} />;
case user.role === "editor":
return <RequestLockButton project={project} />;
case user.role === "viewer":
return <ViewerActions project={project} />;
default:
return null;
}
}
project.archived short-circuits everything else, which is exactly the precedence you want. Try expressing that with a lookup map and you’ll end up with a guard if on top anyway.
Use case 3: numeric ranges
Lookup objects can’t express ranges. switch(true) can.
function PasswordStrengthLabel({ score }: { score: number }) {
switch (true) {
case score >= 90:
return <Label tone="green">Strong</Label>;
case score >= 70:
return <Label tone="lime">Good</Label>;
case score >= 40:
return <Label tone="amber">Weak</Label>;
default:
return <Label tone="red">Very weak</Label>;
}
}
Same pattern works for HTTP status families, file size buckets, age groups, anything ordinal.
Use case 4: form submit state
Disabled, submitting, success, error, idle. Five states, two booleans, one enum. Perfect fit.
function SubmitButton({ form }: { form: FormState }) {
switch (true) {
case form.isSubmitting:
return <Button disabled>Saving…</Button>;
case form.justSubmitted:
return (
<Button disabled tone="success">
Saved
</Button>
);
case !form.isDirty:
return <Button disabled>No changes</Button>;
case !form.isValid:
return <Button disabled>Fix errors to save</Button>;
default:
return <Button onClick={form.submit}>Save</Button>;
}
}
Each line is a sentence: “if submitting, show saving.” The default is the happy path, which is a nice property: it’s hard to forget.
Use case 5: discriminated unions inside reducers
switch(true) shines when you need to combine a discriminator with extra conditions a regular switch can’t express:
function notificationReducer(state: State, action: Action): State {
switch (true) {
case action.type === "DISMISS_ALL":
return { ...state, items: [] };
case action.type === "DISMISS" && state.items.length === 1:
return { ...state, items: [], lastDismissedAt: Date.now() };
case action.type === "DISMISS":
return {
...state,
items: state.items.filter((n) => n.id !== action.id),
};
case action.type === "ADD" && state.items.length >= 5:
return {
...state,
items: [...state.items.slice(1), action.notification],
};
case action.type === "ADD":
return { ...state, items: [...state.items, action.notification] };
default:
return state;
}
}
Plain switch (action.type) forces nested ifs for the “and also…” cases. switch(true) keeps the precedence visible.
When to use a lookup map instead
If you’re just mapping a single value to a component, skip switch(true) entirely:
const ICONS = {
success: CheckIcon,
error: XIcon,
warning: AlertIcon,
info: InfoIcon,
} as const;
function StatusIcon({ kind }: { kind: keyof typeof ICONS }) {
const Icon = ICONS[kind];
return <Icon />;
}
Lookup maps are O(1), exhaustively type-checkable with Record<Kind, ...>, and impossible to get the order wrong on. Use them whenever the branching is one-variable, no-ranges, no-precedence.
Gotchas
- No fall-through. Always
return(orbreak) from each case. In React rendering this is automatic; in reducers it’s a real risk. - Don’t switch on a non-boolean.
switch (true)only works because===comparison againsttrueis whatswitchdoes.case 1 < 2:works;case "yes":does not. - Lint may complain.
default-case-lastandno-fallthroughrules are still useful.no-restricted-syntaxconfigs that banSwitchStatementwill flag this; that’s a team preference call, not a correctness issue. - It’s still imperative control flow. If a branch grows past a few lines, extract a function and call it from the case body.
TL;DR
switch(true) is an ordered list of ifs with less syntactic noise. Use it when precedence matters and you have more than two or three branches. Use a lookup map for single-value dispatch. Use a regular switch when you actually have a discriminator. The pattern doesn’t replace any of those; it fills the gap between them.
Stay in touch
Don't miss out on new posts or project updates. Hit me up on X (Twitter) for updates, queries, or some good ol' tech talk.
Follow @zkMake