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 the create-what --template=fullstack scaffold ships: it serves src/ as native ES modules and its generated notes tell you to write views with h() 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

ParameterTypeDescription
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 html tagged template literal that is easier to read than nested h() calls.
  • Component children are passed via props.children following React's semantics: 0 children = undefined, 1 child = the child directly, N children = an array.