Components

Components are functions that return JSX. They're the building blocks of your UI, letting you split the interface into reusable, independent pieces.

Defining Components

A component is a function that returns JSX:

function Greeting() {
  return <h1>Hello, World!</h1>;
}

// Use it like an HTML tag
<Greeting />

Components must start with a capital letter. Lowercase names are treated as HTML elements.

Props

Pass data to components through props:

function Greeting({ name, className }) {
  return (
    <h1 className={className}>
      Hello, {name}!
    </h1>
  );
}

// Usage
<Greeting name="Alice" className="title" />

Default Values

Use destructuring defaults for optional props:

function Button({
  variant = 'primary',
  size = 'medium',
  children
}) {
  return (
    <button className={`btn btn-${variant} btn-${size}`}>
      {children}
    </button>
  );
}

// Uses defaults
<Button>Click me</Button>

// Override defaults
<Button variant="secondary" size="large">Submit</Button>

Spreading Props

Pass remaining props to child elements:

function Input({ label, ...rest }) {
  return (
    <label>
      {label}
      <input {...rest} />
    </label>
  );
}

<Input
  label="Email"
  type="email"
  placeholder="you@example.com"
  required
/>

Children

Content between component tags is passed as children:

function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}

<Card title="Welcome">
  <p>This is the card content.</p>
  <button>Learn More</button>
</Card>

State in Components

Use signals for component state:

import { signal } from 'what-framework';

function Counter() {
  const count = signal(0);

  return (
    <div>
      <span>Count: {count()}</span>
      <button onClick={() => count.set(c => c + 1)}>
        Increment
      </button>
    </div>
  );
}

Components Run Once

Unlike React, What components only run once when first rendered. Updates happen through signals, not by re-running the component function. This is a key difference from React's mental model.

It has one consequence worth internalizing now: a prop passed as a value is frozen forever. <Greeting name={name()} /> reads the signal once, at the call site, and the child will still say "Alice" after name.set('Bob'). Pass the signal itself (name={name}) and read it as name() in the child, or pass a thunk (name={() => name()}). See Pass Signals, Not Values below.

Composition Patterns

Compound Components

Create related components that work together:

function Tabs({ children }) {
  const activeTab = signal(0);

  return (
    <div className="tabs">
      {children(activeTab)}
    </div>
  );
}

function TabList({ activeTab, children }) {
  return (
    <div className="tab-list">
      {children.map((child, i) => (
        <button
          key={child}
          className={() => activeTab() === i ? 'active' : ''}
          onClick={() => activeTab.set(i)}
        >
          {child}
        </button>
      ))}
    </div>
  );
}

function TabPanels({ activeTab, children }) {
  return (
    <div className="tab-panels">
      {() => children[activeTab()]}
    </div>
  );
}

// Usage: Tabs calls children(activeTab), so pass a function
<Tabs>
  {(activeTab) => (
    <>
      <TabList activeTab={activeTab}>
        <span>One</span>
        <span>Two</span>
      </TabList>
      <TabPanels activeTab={activeTab}>
        <p>First panel</p>
        <p>Second panel</p>
      </TabPanels>
    </>
  )}
</Tabs>

Function-as-children is what makes this pattern work in a run-once framework. There is no re-render to thread the active index back down, so the parent hands its activeTab signal to the children instead, and each child subscribes to it where it needs it.

Render Props

Pass a function as children for flexible rendering:

function Toggle({ children }) {
  const on = signal(false);
  const toggle = () => on.set(v => !v);

  return children({ on, toggle });
}

// Usage
<Toggle>
  {({ on, toggle }) => (
    <button onClick={toggle}>
      {() => on() ? 'ON' : 'OFF'}
    </button>
  )}
</Toggle>

How JSX Compiles

With the What compiler (the Vite and SPA setup), JSX is transformed into direct DOM operations: static HTML is extracted into cloneable templates and dynamic parts are wrapped in fine-grained effects. The compiler is optional. Without it, JSX goes through the runtime h() path, which is correct but does not do keyed list reconciliation, and the fullstack scaffold has no compiler and no JSX at all (you write h() directly there).

Here is what the compiler actually emits:

// Your JSX:
<div className="card">
  <p>Count: {count()}</p>
</div>

// Compiler output:
import { _$template, insert as _$insert } from "what-framework/render";

const _tmpl$0 = _$template('<div class="card"><p>Count: <!--$--></p></div>');

export 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 are worth reading closely. _$template(...) returns a factory, so _tmpl$0() hands back a fresh clone each time. And the <!--$--> in the template string is the anchor: it marks the exact position the dynamic child occupies, which is the third argument to _$insert, so the effect can update that one spot without touching its siblings. There is no separate render() function, the component body is the render.

Fine-Grained Rendering

Static HTML is extracted into templates that are cloned once. Dynamic expressions like {count()} are wrapped in effects that update only the specific DOM nodes that depend on them. There is no virtual DOM, no diffing, and no re-rendering of the entire component. The compiler does the work at build time so the runtime stays minimal.

JSX Basics

JSX is syntactic sugar for creating elements. Here are the key things to know:

Expressions

Use curly braces for JavaScript expressions:

const name = 'Alice';
const items = ['Apple', 'Banana', 'Cherry'];

<div>
  {/* Variables */}
  <h1>Hello, {name}!</h1>

  {/* Expressions */}
  <p>{1 + 1}</p>

  {/* Function calls */}
  <p>{name.toUpperCase()}</p>

  {/* Arrays */}
  <ul>
    {items.map(item => <li key={item}>{item}</li>)}
  </ul>
</div>

Give the .map() callback a key. With one, the compiler lowers the map to keyed reconciliation and only touches the rows that actually changed. Without one it warns at build time and rebuilds every row on each update. Use something stable on the item itself (item.id, or the item when it is a unique string, as here) rather than the index, which is a position and cannot survive a reorder.

Attributes

Some differences from HTML:

// className instead of class
<div className="container">

// camelCase for multi-word attributes
<input tabIndex={1} autoFocus />

// style as an object
<div style={{ color: 'red', fontSize: '16px' }}>

// boolean attributes
<input disabled />
<input disabled={true} />
<input disabled={isDisabled()} />

Two things to watch. A numeric attribute value has to be an expression, so tabIndex={1}, not tabIndex=1, which is a JSX syntax error. And unlike React, What does not append px to numbers in a style object: fontSize: 16 is rejected by the CSSOM and silently dropped, so write the unit yourself. Genuinely unitless properties such as opacity and zIndex take numbers fine.

Fragments

Return multiple elements without a wrapper:

function List() {
  return (
    <>
      <li>First</li>
      <li>Second</li>
      <li>Third</li>
    </>
  );
}

Best Practices

1. Keep Components Small

If a component gets too large, split it into smaller pieces:

// Instead of one large component
function Dashboard() {
  return (
    <div>
      <Header />
      <Sidebar />
      <MainContent />
      <Footer />
    </div>
  );
}

2. Lift State Up When Needed

If two components need to share state, lift it to their common parent:

function Parent() {
  const value = signal('');

  return (
    <div>
      <Input value={value} />
      <Display value={value} />
    </div>
  );
}

function Input({ value }) {
  return <input value={value} onInput={e => value.set(e.target.value)} />;
}

function Display({ value }) {
  return <p>You typed: {value}</p>;
}

3. Pass Signals, Not Values

For reactive props, pass the signal itself:

// GOOD - Child can update the signal
<Counter count={count} />

function Counter({ count }) {
  return <button onClick={() => count.set(c => c + 1)}>{count()}</button>;
}