CHART/DOCS/SIGNALS

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 .value directly in the 's item template - the compiler detects the read and binds each item. A 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.