effect

effect(fn) runs fn once and again every time anything it read changes. Use it for side-effects that mirror reactive state into the outside world — logging, persistence, subscriptions, third-party libraries. Prefer memo / derived when you can derive a value instead of imperatively pushing it.

Arguments

name type description
fn () => void function whose tracked reads become the effect's dependencies. Re-runs when any tracked signal changes, at the end of the current update batch — after memos settle (use syncEffect when the body must run during creation or ahead of regular effects).

Returns: void. The effect lives until its owner is disposed; pair imperative setup with cleanup to detach listeners or timers when the scope tears down. For deferred callbacks (timers, promise continuations) that should become no-ops once the owner is gone, see owned.

API shape

import { effect, signal } from 'pota'

const count = signal(0)

effect(() => {
	// runs once now, and again whenever `count` changes
	console.log(count.read())
})

Examples

Log on change

Run a side-effect every time tracked state updates — the effect fires at the end of each click's update batch.

import { effect, render, signal } from 'pota'

function App() {
	const count = signal(0)
	const log = signal('click to start')

	effect(() => {
		log.write(`count is now ${count.read()}`)
	})

	return (
		<div>
			<button on:click={() => count.update(n => n + 1)}>
				{count.read}
			</button>
			<p>{log.read}</p>
		</div>
	)
}

render(App)

Persist to localStorage

The effect re-runs whenever theme changes and writes the new value to storage. The initial run also performs the first write, so the stored value stays in sync from the moment the component mounts.

import { effect, render, signal } from 'pota'

function App() {
	const theme = signal(localStorage.getItem('theme') ?? 'light')

	effect(() => {
		localStorage.setItem('theme', theme.read())
	})

	return (
		<div>
			<p>theme: {theme.read}</p>
			<button on:click={() => theme.write('light')}>light</button>
			<button on:click={() => theme.write('dark')}>dark</button>
		</div>
	)
}

render(App)

Subscribe with cleanup

cleanup registers a callback to run when the surrounding owner is disposed (a parent unmount, a re-rendered branch, or render()'s returned disposer being called) — the right place to detach listeners opened inside an effect. Move the pointer to see x/y update; unmount the app and the listener detaches.

import { cleanup, effect, render, signal } from 'pota'

function App() {
	const x = signal(0)
	const y = signal(0)

	effect(() => {
		const onMove = e => {
			x.write(e.clientX)
			y.write(e.clientY)
		}
		window.addEventListener('mousemove', onMove)
		cleanup(() => window.removeEventListener('mousemove', onMove))
	})

	return (
		<p>
			pointer at {x.read}, {y.read}
		</p>
	)
}

render(App)

Debounced effect

When an effect should react to rapid signal changes (typing in a search box, dragging a slider) but the side-effect is expensive, debounce it. useTimeout is owner-aware: each effect re-run disposes the previous timer before a new one is scheduled, so only the final keystroke fires the search.

import { effect, render, signal } from 'pota'
import { useTimeout } from 'pota/use/time'

function App() {
	const query = signal('')
	const result = signal('—')

	effect(() => {
		const q = query.read()
		const timer = useTimeout(() => {
			result.write(q ? `searched for "${q}"` : '—')
		}, 400)
		timer.start()
	})

	return (
		<div>
			<input
				placeholder="type a query…"
				on:input={e => query.write(e.currentTarget.value)}
			/>
			<p>{result.read}</p>
		</div>
	)
}

render(App)