Signals and computed
One state primitive. One way to derive state. No hooks.
There is exactly one state primitive: signal(). Exactly one way to derive state: computed(). Neither is a hook, neither takes a dependency array, and neither has a timing rule you can get wrong.
// @runsOn client
import { signal, computed } from '@marisjs/runtime';
type Props = {};
export function Stats(props: Props) {
const price = signal(42);
const discount = signal(0.15);
const total = computed(() => price.value * (1 - discount.value));
function applyCoupon() {
discount.set(0.3);
}
return (
<div>
<p>Price: ${price.value}</p>
<p>Total: ${total.value}</p>
<button onClick={applyCoupon}>Apply coupon</button>
</div>
);
}
Reading and writing
- Read a signal with
count.value- always explicit, never implicit. - Write with
count.set(newValue)- the single canonical write method. computed()re-evaluates whenever any signal it reads changes.
Passing signals around
State flows top-down through props. Pass the signal itself by reference; leaf components read .value and subscribe automatically. There is no Context API, no Provider, no dependency injection. Reactivity follows the props chain at any depth - props.deeply.nested.count.value binds just like props.count.value.
In lists: read 's item template - the compiler detects the read and binds each item. A
.value directly in the computed() that maps to plain objects loses signal identity, so precomputed values do not update reactively inside items. Known tradeoff: passing a theme or locale through several layers of components that never read it is boilerplate. This is a deliberate v1 decision - it costs zero runtime and keeps data flow fully visible. See the FAQ.
Why
.value must be explicit: implicit reads are how stale closures happen in other frameworks. When the read is a visible property access, there is nothing hidden to go stale.