Rendering rules
JSX is a strict subset of TSX. The rules exist to eliminate entire bug classes.
Lists: , not .map()
.map() Inline .map() in JSX is where missing key props originate. 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 && 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 automaticpxon numbers (except unitless properties likeopacity);null/undefinedvalues omit the property; a.valueread 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.