h() Advanced
Prefer JSX when you have a build step
h() is the runtime element factory. With a build step, JSX is lowered by the What compiler into cloneable templates plus insert() calls, which is faster than building each element through h(). Without a build step, h() is the supported way to write views.
Write JSX and let the compiler handle the rest:
// Just write JSX, the compiler optimizes it for you
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// The compiler transforms this into a cloned template plus
// an insert() call, with no intermediate representation.
When Would You Use h()?
- Buildless apps. There is no compiler and no JSX, so
h()is the authoring API. This is the model thecreate-what --template=fullstackscaffold ships: it servessrc/as native ES modules and its generated notes tell you to write views withh()and pass() => ...for anything reactive. - Dynamic tag names. When the element tag is determined at runtime and cannot be expressed as JSX.
When you do have a build step, use JSX. The compiler produces significantly more optimized output than h() calls.
Signature
import { h } from 'what-framework';
const element = h(tag, props, ...children)
Parameters
| Parameter | Type | Description |
|---|---|---|
tag |
string | Function |
An HTML tag name (e.g. 'div') or a component function. |
props |
object | null |
Properties/attributes for the element, or null if none. |
...children |
any[] |
Child elements. Can be strings, numbers, other elements, functions (reactive children), arrays, or null/boolean (ignored). |
Example (No Build Step)
import { h, signal, mount } from 'what-framework';
function Counter() {
const count = signal(0);
return h('div', null,
h('button', { onClick: () => count.set(c => c + 1) },
() => 'Count: ' + count()
)
);
}
mount(h(Counter), '#app');
With a build step, the same component in JSX is far more readable. Keep the reactive
part as a thunk so the two really are equivalent: a bare {count()} is only
live once what-compiler has rewritten it, while the h() version above passes
a thunk and is live on every path.
function Counter() {
const count = signal(0);
return (
<div>
<button onClick={() => count.set(c => c + 1)}>
{() => 'Count: ' + count()}
</button>
</div>
);
}
mount(<Counter />, '#app');
How JSX Actually Works
The What compiler transforms JSX into optimized direct DOM operations. It does not compile to h() calls:
// Your JSX:
import { signal } from 'what-framework';
const count = signal(0, 'count');
function Card() {
return (
<div className="card">
<p>Count: {count()}</p>
</div>
);
}
// What the compiler emits:
import { _$template, insert as _$insert } from 'what-framework/render';
const _tmpl$0 = _$template('<div class="card"><p>Count: <!--$--></p></div>');
function Card() {
const _el$0 = _tmpl$0();
const _el$1 = _el$0.firstChild;
_$insert(_el$1, () => count(), _el$1.firstChild.nextSibling);
return _el$0;
}
Two details that trip people up when they read compiled output:
_$template() returns a factory, so you call it (_tmpl$0())
to get a fresh clone, you do not call .cloneNode() on it; and no
effect() appears in the output because insert() creates the
reactive effect internally for the thunk it is handed.
Notes
- When using a build step (recommended), always prefer JSX. The compiler output is faster than
h()because it separates static HTML into cloneable templates. - For no-build-step usage, What also provides an
htmltagged template literal that is easier to read than nestedh()calls. - Component children are passed via
props.childrenfollowing React's semantics: 0 children =undefined, 1 child = the child directly, N children = an array.