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

effect

Runs a synchronous function now and whenever a signal it read changes; returns the function that stops it.

reactivity Since 1.0.0 No import @meekojs/meeko Source code

Se autoimporta en las aplicaciones del framework.

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

effect(fn) bridges signals and the outside world: it runs fn immediately, records the signals it reads and reruns it when any changes. Use it to synchronize something Meeko does not render (the document title, a third-party library, localStorage, a connection) with state. To calculate a value, use computed; to render, use the template.

In a component script, the effect belongs to the component and stops on unmount. It is exported from @meekojs/meeko and auto-imported in framework applications.

Usage#

meeko
<!-- app/components/SearchBox.meeko -->
<script lang="ts">
const query = signal("");
effect(() => {
  if (typeof document === "undefined") return;
  document.title = query.value ? `Search: ${query.value}` : "Search";
});
</script>

<template><input type="search" model="query"></template>

The first run occurs when the effect is created; every letter the user types changes query and reruns it in the browser.

Type#

ts
function effect<const F extends () => unknown>(
  fn: SyncEffectCallback<F>,
  source?: EffectSource,
): () => void

ReadonlySignal

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

Parameters#

Parameter Type Default Description
fn () => unknown required Synchronous callback reading signals. A callback returning a promise (async) is a type error and, at runtime, a TypeError.
source EffectSource none Expression location for DevTools. Added by the compiler in development; do not write it yourself.

Return value#

A () => void function that stops the effect: it stops observing its signals and never runs again. In a component, you need not call it because the effect stops on unmount; use it to stop earlier, or for an effect created outside a root.

Examples#

Stop an effect#

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

const count = signal(0);
const seen: number[] = [];
const stop = effect(() => { seen.push(count.value); });

count.value = 1;
stop();
count.value = 2; // seen remains [0, 1]

Synchronize an external library and dispose of it#

onCleanup inside a component disposes of the resource on unmount; the effect reapplies the option when the signal changes:

meeko
<!-- app/components/Chart.meeko -->
<script lang="ts">
import { createChart } from "../lib/chart";

const props = defineProps<{ points: number[] }>();
const canvas = useElementRef("canvas");
onMounted(() => {
  const chart = createChart(canvas.value!);
  effect(() => chart.update(props.points));
  return () => chart.destroy();
});
</script>

<template><canvas ref:canvas width="400" height="200"></canvas></template>

The effect created inside onMounted still belongs to the component: Meeko stops it with the component.

Asynchronous work from a synchronous effect#

The effect reads signals synchronously and starts the request; cancellation and errors are your responsibility:

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

const query = signal("");
const results = signal<string[]>([]);
let controller: AbortController | undefined;

effect(() => {
  const q = query.value.trim(); // the dependency is tracked here, before any await
  controller?.abort();
  if (!q) { results.value = []; return; }
  const current = new AbortController();
  controller = current;
  fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: current.signal })
    .then((response) => response.json() as Promise<string[]>)
    .then((list) => { results.value = list; })
    .catch((error: unknown) => { if (!current.signal.aborted) console.error(error); });
});

In a framework component, useRpcQuery or useApi with debounceMs and enabled (since 1.7) already handle this, with cancellation, SSR and hydration.

Read a signal without depending on it#

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

const query = signal("");
const page = signal(1);
// Se repite al cambiar query, no al cambiar page.
effect(() => console.log(query.value, untrack(() => page.value)));

Common mistakes#

An async callback#

Incorrectts
effect(async () => {
  const response = await fetch(`/api/users?q=${query.value}`);
  users.value = await response.json();
});

meeko check reports TS2345: Argument of type '() => Promise<void>' is not assignable to parameter of type 'never' and, at runtime, the effect throws TypeError: Meeko effect callbacks must be synchronous. Reads after an await would not be recorded. Read signals first and start the promise without returning it:

Correctts
effect(() => {
  const q = query.value;
  void fetch(`/api/users?q=${q}`).then((response) => response.json()).then((list) => { users.value = list; });
});

Write the signal the effect reads#

Incorrectts
effect(() => {
  count.value = count.value + 1;
});

Each write triggers the effect again, and Meeko stops it with Error: Reactive update cycle exceeded 1000 iterations. If a value depends on another, it is a computed:

Correctts
const next = computed(() => count.value + 1);

Use effect to derive state#

Incorrectts
const total = signal(0);
effect(() => { total.value = price.value * quantity.value; });

This works, but total is another writable signal: it travels in component state, someone can write it and it is briefly out of sync. A computed value is a computed:

Correctts
const total = computed(() => price.value * quantity.value);

Access the DOM or browser APIs without checking#

The effect also runs once in SSR.

Incorrectts
effect(() => { window.scrollTo(0, offset.value); });

In SSR it throws ReferenceError: window is not defined and the page fails. Check the environment, or create the effect in onMounted, which only runs in the browser:

Correctts
onMounted(() => {
  effect(() => { window.scrollTo(0, offset.value); });
});

Notes#

  • SSR. On the server, the effect runs only the first time, on creation. It does not run during a client navigation's dry run (the preliminary render loading the new page's data).
  • Order. Pending effects run in creation order: a parent effect, such as an if:when effect, runs before those it created. Multiple successive writes run the effect multiple times; group them with batch.
  • Errors. An exception in an effect created during setup fails rendering. One occurring in reaction to a write propagates to the writer (for example, the on:click handler), which sends it to the nearest provideErrorHandler. If multiple observers fail in the same round, they are grouped in an AggregateError.
  • Lifecycle. Inside a component, the effect stops on unmount, page change or when its if:when or each:of row disappears. Outside a root (a module or test), it lives until you call its returned function, or create it inside createRoot.
  • No onUpdated. Meeko has no update hook: an effect reading signals is how you react to their changes.

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/effect.md