Renuel provides a flexible, type-safe system for building React UIs with plain functions—harnessing the full expressive power of JavaScript while avoiding the context-switching syntax of JSX.
div({ className: "greeting" }, "Hello ", em$("world"))- Just functions: Build React components without JSX.
- Flexible and concise: Factory variants allow props and children to be passed with minimal syntax, reducing boilerplate without compromising type safety.
- Precision composition: A polymorphic component approach that delivers convenience and correctness
- Type safety by default: Excess props are disallowed, and prop conflicts must be resolved explicitly.
- Expressive JavaScript: Take full advantage of JavaScript and TypeScript features with no extra syntax or context-switching.
npm install renuel # or yarn, pnpm, etc.Here's an example of using Renuel to create a simple counter app:
import { useReducer } from "react";
import { createRoot } from "react-dom/client";
import { button, component, strong$ } from "renuel";
const { App$ } = component("App", () => {
const [count, onClick] = useReducer((x) => x + 1, 0);
return button({ onClick }, "Count: ", strong$(count));
});
const rootEl = document.getElementById("root");
if (rootEl) {
const root = createRoot(rootEl);
root.render(App$());
}Here's a simple Button component with a variant prop and children as the label:
import { component, button$ } from "renuel";
const { Button, Button$ } = component(
"Button",
({
variant = "secondary",
children,
}: {
variant?: "primary" | "secondary";
children?: React.ReactNode;
}) =>
button$(
{
style:
variant === "primary"
? {
background: "blue",
color: "white",
padding: "0.5rem 1rem",
borderRadius: 4,
}
: {
background: "lightgray",
padding: "0.5rem 1rem",
borderRadius: 4,
}
},
children
)
);
// Usage — props + children
Button({ variant: "primary" }, "Click me")
// Usage — skip props (defaults to "secondary" variant)
Button$("Cancel")Polymorphic components let you reuse styling while rendering different
underlying elements. The canonical example is a Button component that can be
rendered as an HTML button element or as an a element, but looks the same
either way.
Renuel makes this type of composition explicit through a render prop, ensuring both flexibility and type safety.
To make the Button polymorphic, you can change children to a render prop (aka Function as Child Component):
import { component, button$, _a, _button$ } from "renuel";
const { Button, Button$ } = component(
"Button",
({
variant = "secondary",
children
}: {
variant?: "primary" | "secondary";
children: (props: { style: React.CSSProperties }) => React.ReactNode;
}) =>
children({
style:
variant === "primary"
? {
background: "blue",
color: "white",
padding: "0.5rem 1rem",
borderRadius: 4,
}
: {
background: "lightgray",
padding: "0.5rem 1rem",
borderRadius: 4,
}
})
);
// Usage — render as a link
Button({ variant: "primary" }, _a({ href: "/docs" }, "Get started"));
// Usage — render as a plain button
Button$(_button$("Default button"));Each tag (or custom component) comes with four factory variants:
tag(Component): standard factory; accepts props + children.tag$(Component$): skip-props factory; accepts children only._tag(_Component): partial factory; returns a new factory after fixing some props._tag$(_Component$): partial skip-props factory; like_tag, but starts with children.
Tip
A way to remember the naming convention is:
$means "skip props", i.e. first argument is a child_means "partial", i.e. returns another factory
Example with div:
div({ className: "foo" }, "Hello") // standard
div$("Hello") // skip-props
_div({ id: "foo" }, "Hello")({ className: "foo" }) // partial
_div$("Hello")({ className: "foo" }) // partial skip-propsNote
In the example above, invoking the curried factories with additional props is for demonstration purposes only. In practice, you’d typically pass the curried factory as a child to a polymorphic component, which is then responsible for supplying the remaining props.
This pattern applies to both native tags and custom components, making composition predictable and type-safe with minimal syntax.
If you already use JSX, Renuel will feel familiar — but with less syntax overhead and stronger type guarantees. Here are a few common patterns compared directly:
// JSX
<ul>{items.map(i => (<li key={i.id}>{i.name}</li>))}</ul>
// Renuel
ul$(items.map(i => li({ key: i.id }, i.name)))// JSX
<div>{isLoggedIn ? <p>Welcome back!</p> : <p>Please log in</p>}</div>
// Renuel
div$(isLoggedIn ? p$("Welcome back!") : p$("Please log in"))// JSX
<Button>{({ style }) => <a href="/docs" style={style}>Docs</a>}</Button>
// Renuel
Button(_a({ href: "/docs" }, "Docs"))// JSX
<div style={{ background: "blue", color: "white" }}>Hello world</div>
// Renuel
div({ style: { background: "blue", color: "white" } }, "Hello world")// JSX
<footer>© 2025 MyCompany. All rights reserved.</footer>
// Renuel
footer$("© 2025 MyCompany. All rights reserved.")