React Renderable
Declarative render primitives for React. Every primitive shares one convention, so learning one teaches you all of them.
Installation
npm install react-renderableThe problem
JSX has no syntax for branching, so conditions are expressed with the operators of the language: ternaries and &&. They work, but they nest badly. A second condition turns the expression inside out, the branches drift away from the conditions that select them, and formatting decides where the reader's eye lands.
{isLoading ? <Spinner /> : error ? <ErrorPanel error={error} /> : <Results rows={rows} />}The primitives here give those branches a shape. Each one is a component, so a condition reads as an element among elements rather than as punctuation between them.
<Switch>
<Switch.Case when={isLoading}>{Spinner}</Switch.Case>
<Switch.Case when={!!error}>{() => <ErrorPanel error={error!} />}</Switch.Case>
<Switch.Default>{() => <Results rows={rows} />}</Switch.Default>
</Switch>The Renderable convention
Every slot accepts both forms: the component itself, or its element.
<Wrap components={[ThemeProvider]}>{children}</Wrap>
<Wrap components={[<ThemeProvider theme="dark" />]}>{children}</Wrap>Use the component form by default. Use the element form when the renderable needs props. The two are told apart by typeof === "function": a component is always a function, a ReactNode never is — so there is no heuristic and no ambiguity.
type Renderable = ComponentType<{ children: ReactNode }> | ReactNode;The payoff of the component form is that it is not evaluated until it is rendered. A branch that is not taken costs nothing, and code that would crash outside its branch never runs. The element form gives that up in exchange for props: the element exists as soon as the JSX is read, whether or not it ends up on screen.
Which props take a Renderable
Anything rendered conditionally, so it can stay unevaluated until it is actually needed. Props that always pass through — the children of Wrap and Inject — stay a plain ReactNode.
The rule is worth stating because it is also the rule for your own components. A slot that may not render is a Renderable; a slot that always renders is a node. Following it keeps every component in a codebase answering the same question the same way, which is the entire point of a convention.
A note on inline arrows
Passing an inline arrow as a renderable gives React a new component type on every render, which remounts that subtree. It is free for stateless nodes; hoist it out of the render for anything holding state.
const Total = ({ amount }: { amount: number }) => <strong>{amount}</strong>;
<Guard guardIf={!invoice}>{() => <Total amount={invoice!.total} />}</Guard>In the snippet above the arrow is the deferral and Total is the component: because Total is defined once, outside the render, remounting the arrow costs a new element but never resets state inside it. The pattern to avoid is the opposite one — declaring a stateful component inline, where every parent render throws its state away.