Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

signal

Creates an observable, writable value with .value; writing a different value notifies the template, computed values and effects that read it.

reactivity Since 1.0.0 No import @meekojs/meeko Source code

Se autoimporta en las aplicaciones del framework.

ts
import { signal } from "@meekojs/meeko";

signal(initial) creates Meeko's most basic state: an object with a value property. Reading value in a template, computed or effect registers the dependency; assigning a different value notifies only those that read it. There is no virtual DOM: the compiled template updates exactly the node or attribute depending on the signal.

In a component's <script>, top-level signals are also component state: the server serializes them with the HTML (renderWithState), hydration restores them and HMR preserves them by name. It is exported from @meekojs/meeko and auto-imported in framework applications.

Usage#

meeko
<!-- app/components/Counter.meeko -->
<script lang="ts">
const count = signal(0);
</script>

<template>
  <button type="button" on:click="() => count.value++">Clicked {count.value} times</button>
</template>

Each click assigns a new number to count.value and Meeko updates only the button text.

Type#

ts
function signal<T>(initial: T): Signal<T>

Signal

ts
interface Signal<T> extends ReadonlySignal<T> {
	value: T
}

ReadonlySignal

ts
interface ReadonlySignal<T> {
	readonly value: T
}

Parameters#

Parameter Type Default Description
initial T required Initial value. T is inferred from it: signal(0) is Signal<number>. For a union or empty value, write the type: signal<User | null>(null), signal<string[]>([]).

Return value#

A Signal<T>, an object with a single property:

Property Type Description
value T Read: returns the current value and, if there is an active observer (the template, a computed, an effect), subscribes it. Write: if the new value differs according to Object.is, stores it and immediately runs observers that read it.

Signal<T> is assignable to ReadonlySignal<T>, the read-only type returned by computed and composables: use it to expose a signal without allowing others to write it.

Examples#

Explicitly typed state#

ts
import { signal } from "@meekojs/meeko";

interface User { id: string; name: string }

const user = signal<User | null>(null);
const tags = signal<string[]>([]);

user.value = { id: "1", name: "Ada" };
tags.value = [...tags.value, "admin"]; // a new array: notifies observers

A form control with model#

model creates a two-way binding between a control and a writable signal of the control's type:

meeko
<script lang="ts">
const name = signal("");
const accepted = signal(false);
</script>

<template>
  <input model="name" placeholder="Your name">
  <label><input type="checkbox" model="accepted"> I accept</label>
  <button bind:disabled="!accepted.value || !name.value.trim()">Send</button>
</template>

Expose a read-only signal from a composable#

ts
// app/composables/counter.ts
import { signal } from "@meekojs/meeko";
import type { ReadonlySignal } from "@meekojs/meeko";

export function useCounter(start = 0): { count: ReadonlySignal<number>; increment: () => void } {
  const count = signal(start);
  return { count, increment: () => { count.value++; } };
}

The caller can read count.value, but assigning it is a type error (TS2540).

Component state transferred to the browser#

meeko
<!-- app/components/Tabs.meeko -->
<script lang="ts">
const props = defineProps<{ initial: "perfil" | "actividad" }>();
const tab = signal(props.initial);
</script>

<template>
  <button type="button" on:click="() => tab.value = 'perfil'" bind:aria-pressed="String(tab.value === 'perfil')">Profile</button>
  <button type="button" on:click="() => tab.value = 'actividad'" bind:aria-pressed="String(tab.value === 'actividad')">Activity</button>
</template>

Server HTML uses the initial tab, the tab signal travels in component state and hydration restores it before claiming the HTML: there is no mismatch.

Common mistakes#

Mutate a signal's object or array#

A signal only notifies when assigned a different value (Object.is). push or changing a property modifies the same object: nobody is notified and there is no error.

Incorrectts
items.value.push(item);
user.value.name = "Grace";

Assigning the same object afterward does not notify either, because Object.is(objeto, objeto) is true. Always assign a new value:

Correctts
items.value = [...items.value, item];
user.value = { ...user.value, name: "Grace" };

Forget .value#

The signal is an object; its value is in value.

Incorrectmeeko
<button on:click="() => count++">{count.value}</button>

meeko check reports TS2588: Cannot assign to 'count' because it is a constant. In a condition, if:when="count > 0" produces TS2365: Operator '>' cannot be applied to types 'Signal<number>' and 'number'.

Correctmeeko
<button on:click="() => count.value++">{count.value}</button>

Write to a computed#

Incorrectts
const double = computed(() => count.value * 2);
double.value = 4;

meeko check reports TS2540: Cannot assign to 'value' because it is a read-only property, and since 1.7 assignment throws TypeError at runtime. Write to the source signal:

Correctts
count.value = 2; // double.value pasa a 4

Store a value JSON cannot represent in state#

Top-level script signals travel as JSON with server HTML.

Incorrectts
const when = signal(new Date());

Rendering fails with the component and signal names, such as app/pages/agenda.meeko: the signal "when" cannot be sent to the browser with the SSR state (Non-plain object at $.when). Store a serializable value and derive the object:

Correctts
const when = signal(new Date().toISOString());
const date = computed(() => new Date(when.value));

Notes#

  • Server and browser. In SSR the signal works the same, but HTML is sent only once: writes after rendering do not reach the browser. Template updates occur in the browser after hydration.
  • Transferred state. Only writable top-level script signals travel. A computed, a constant or a signal stored in an object is not part of state; an undefined does travel since 1.5. For state shared by multiple components, use useState.
  • HMR. When editing a component in meeko dev, Meeko preserves its writable signals by name; computed values are recalculated.
  • Synchronous updates. Each write immediately runs affected observers in creation order. To group multiple writes into one round, use batch.
  • Equality. NaN equals NaN and -0 differs from 0, as in Object.is. Writing the same value does nothing.
  • Loops. An observer writing the signal it reads repeats until Meeko stops it with Reactive update cycle exceeded 1000 iterations.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/reactivity/signal.md