CHART/DOCS/RENDERING

Rendering rules

JSX is a strict subset of TSX. The rules exist to eliminate entire bug classes.

Lists: , not .map()

Inline .map() in JSX is where missing key props originate. makes key a required parameter of the construct itself - there is no code path that skips it.

// @runsOn client
import { signal } from '@marisjs/runtime';

type Props = {};

export function Rows(props: Props) {
  const rows = signal([
    { id: 1, label: 'first', value: 10 },
    { id: 2, label: 'second', value: 20 },
  ]);

  function remove(id: number) {
    rows.set(rows.value.filter((row) => row.id !== id));
  }

  return (
    <For each={rows.value} key={(row) => row.id}>
      {(row) => (
        <div class="row">
          <span>{row.label}: {row.value}</span>
          <button onClick={() => remove(row.id)}>x</button>
        </div>
      )}
    </For>
  );
}
Reactivity tip: to update values inside a list item reactively, read .value directly in the item template. Per-item state can live in signals held by the item objects themselves.

Conditionals: ternary only

Conditional rendering is exactly one form: a ternary with an explicit null on the empty branch. condition && silently renders 0, "", or NaN when the condition is falsy-but-not-boolean - marisjs rejects the pattern outright.

Handlers and attributes

  • Handlers: named functions in the component body, or inline arrows. No string names. Server components reject event handlers entirely (SERVER_EVENT_HANDLER) - their output is static HTML.
  • Attributes: class, href, and any other attribute accept expressions - they evaluate to the rendered value.
  • Style: a string (style="color: red") or an object (style={{ color: 'red', padding: '1rem' }}). Object keys are camelCase, serialized to kebab-case CSS with automatic px on numbers (except unitless properties like opacity); null/undefined values omit the property; a .value read updates the style live.
  • Props: always accessed as props.field. Never destructured in the signature.
  • Exactly one root element per component. Fragments are fine inside the tree, not as the root.
// @runsOn client
import { signal } from '@marisjs/runtime';

type Props = {};

export function Field(props: Props) {
  const value = signal('');

  function handleInput(e: any) {
    value.set(e.target.value);
  }

  function reset() {
    value.set('');
  }

  return (
    <div>
      <input type="text" value={value.value} onInput={handleInput} />
      <button onClick={reset} disabled={value.value === ''}>clear</button>
      <span style={{ opacity: value.value ? 1 : 0.3 }}>live</span>
    </div>
  );
}
Rule of thumb: if a construct hides control flow, it is banned. The rendering rules exist so that every branch of your UI is visible in the source.