react-jsx-sucks
v0.1.3
Published
JSX kinda sucks. It needs `If`, `ElseIf`, `Else`, `Switch`, `Show`, `For`.
Downloads
31
Readme
react-jsx-sucks
JSX kinda sucks. It needs If, ElseIf, Else, Switch, Show, For.
Declarative control-flow components for React — no more .map() and ternaries buried in your markup. Every component takes function children, so a branch's body never runs until that branch is actually selected. That makes them safe under the React Compiler and free of the eager-evaluation footgun that breaks react-if and friends.
Pass the values a branch needs through args, and they arrive non-null — no user! assertions, no manual if (!user) return null.
<Show when={user} args={[user]} fallback={() => <span>Not signed in</span>}>
{(user) => <span>{user.name}</span>}
</Show>Why function children
React evaluates all JSX children before the parent component runs. So with element-children control-flow (<If cond><span>{user.name}</span></If>), user.name is read even when cond is false — that's why those libraries crash on null and why the React Compiler, which hoists and reorders based on standard semantics, mishandles them.
Here the body is a closure ({() => ...}). It isn't called until the guard passes, so:
- null-safe —
user.nameis never touched whenuseris null. - compiler-safe — the compiler sees an ordinary deferred call it won't hoist past the guard.
- cheap — the skipped branch's element tree is never built.
The rule: function children = safe. Element children = the bug.
Passing args
If, ElseIf, Else, Case, Default, and Show take an args prop — a tuple of values that gets spread into the branch body:
<Show when={user} args={[user]}>
{(user) => <span>{user.name}</span>}
</Show>Two things fall out of this for free:
- Non-null narrowing. Each arg is passed through
NonNullable, so a body guarded bywhen={user}receivesuserwithnull/undefinedstripped from its type. Nouser!, noif (!user) return null. - Stale-closure immunity. The body reads its parameters rather than variables it closes over.
Condition/Switch/Showreadargsoff the same element aswhen, so the guard and the body can never disagree — even if the React Compiler failed to invalidate a closure.
args is optional. Omit it and the body is a plain zero-arg function, exactly as before.
Install
npm install react-jsx-sucksReact 16.8+ is a peer dependency (bring your own).
Components
| Component | Purpose |
|-----------|---------|
| Condition | Wraps If / ElseIf / Else; renders the first truthy branch. |
| If, ElseIf, Else | Branch markers inside Condition. |
| Show | Single guarded branch with optional fallback. |
| Switch | Wraps Case / Default; matches on by equality. |
| Case, Default | Branch markers inside Switch. |
| For | List rendering with automatic key injection. |
Condition / If / ElseIf / Else
The first branch whose when is truthy renders; Else is the fallback.
<Condition>
<If when={loading}>{() => <Spinner />}</If>
<ElseIf when={error} args={[error]}>{(error) => <span>Failed: {error.message}</span>}</ElseIf>
<Else>{() => <span>Hi there</span>}</Else>
</Condition>Pass args on any branch to receive its guard values non-null (see Passing args).
Show
<Show when={user} args={[user]} fallback={() => <span>Not signed in</span>}>
{(user) => <span>{user.name}</span>}
</Show>fallback is optional; without it, a falsy when renders nothing. Because fallback runs on the falsy path — where the guard values may be null — it takes no args.
Switch / Case / Default
Case matches when === on. Default renders when nothing matched (like a JS switch's default).
<Switch on={status}>
<Case when="loading">{() => <Spinner />}</Case>
<Case when="error">{() => <span>Something broke</span>}</Case>
<Case when="done">{() => <span>All set</span>}</Case>
<Default>{() => <span>Unknown status</span>}</Default>
</Switch>Case and Default also take args — same as the If family (see Passing args).
Fall-through. Once a Case matches, it and every following Case render until one carries break — matching C/JS switch semantics.
<Switch on={tier}>
<Case when="trial">{() => <span>Trial banner</span>}</Case>
<Case when="paid" break>{() => <span>Paid features</span>}</Case>
<Default>{() => <span>No tier</span>}</Default>
</Switch>
// tier="trial" → renders "Trial banner" AND "Paid features", then stops at break.For
Renders a list. Children is a function (item, index) => element. Keys are injected automatically.
<For each={items} fallback={() => <span>No items</span>}>
{(item, i) => <li>{i + 1}. {item.label}</li>}
</For>Key resolution, in order:
- Hand-set key wins.
<li key={item.slug}>is left untouched. item[keyField]— defaults toid; override withkeyField="uuid".- Array index — last resort, with a dev-mode warning so you notice.
{/* auto key from item.id */}
<For each={items}>{(item) => <li>{item.label}</li>}</For>
{/* custom key field */}
<For each={items} keyField="uuid">{(item) => <li>{item.label}</li>}</For>
{/* hand-set key takes precedence */}
<For each={items}>{(item) => <li key={item.slug}>{item.label}</li>}</For>fallback renders when each is empty, null, or undefined. Because For injects keys via cloneElement, the callback must return a single element (wrap multiple children in one parent).
TypeScript
Fully typed. Switch<T> / Case<T> are generic over the discriminant, so a when of the wrong type is a compile error. For<T> infers the item type from each and constrains keyField to keyof T.
Branch guards are runtime, so TS can't narrow through the closure on its own. Pass the guarded values through args and they arrive non-null:
<Show when={user} args={[user]} fallback={() => <span>Not signed in</span>}>
{(user) => <span>{user.name}</span>} // user is non-null here — no `!`, no manual guard
</Show>License
MIT
