Skip to main content

Effect

Effect is a TypeScript library for typed, composable computations. dziry recognises Effect values structurally and imports the package lazily, so an app that never hands one over never loads a byte of it. This page is the whole of dziry's Effect support in one place.

Four things you can do with Effect today: run it from a handler, inject it through the window layer, validate with it, and stream it into a signal.

:::note Installed, never loaded

dziry carries effect in its dependencies so its types always resolve — the generated route types name Effect<A, E, R> — but the package is imported only when your code actually hands one over: a handler's return value, a layer, a schema, or a source(). An app that never uses one loads zero bytes of it. Installed ≠ loaded. :::

Handlers may return an Effect

A handler that returns an Effect is run on the window's runtime; one that returns nothing is an ordinary function.

import { Effect } from "effect";

export const addTodo = () =>
Effect.gen(function* () {
const store = yield* AppStore; // a required service — see below
yield* Effect.sync(() => store.commit(events.todoAdded({ id, title })));
draft.set("");
});

dziry detects the returned Effect, runs it to completion, prints the cause when it fails, and stays silent when it is interrupted. Nothing about Effect is required until a handler actually returns one.

The window layer

Dependency injection has one root: the window. <Window layer={layer}> hands dziry a Layer; it builds a ManagedRuntime from it at launch and disposes it when the window closes, so Layer.scoped resources (a store, a socket) open while the first frame paints and their finalizers run on quit.

import { Effect, Layer } from "effect";

export const layer = Layer.scoped(
AppStore,
Effect.acquireRelease(
Effect.promise(() => createStorePromise({ schema })),
(store) => Effect.promise(() => store.shutdownPromise()),
),
);
<Window title="todos" width={560} height={680} layer={layer}>

A handler asks for a service with yield* Tag; the layer satisfies it. One window = one layer = one runtime.

Validate with an Effect schema

validate={schema} accepts three shapes: a Standard Schema (Zod 4, Valibot, ArkType), an Effect schema, or a plain (data) => issues function.

<form validate={Login} onSubmit={save}></form>

An Effect schema is recognised by its ast and converted with Effect's own Schema.standardSchemaV1 after a lazy import — so validate={Login} works unwrapped, without dziry importing effect itself.

source() — a signal from a Stream

source() is dziry's push primitive — a signal fed from outside. The Effect shape is a subscribe that returns a Stream; dziry recognises it structurally and forks Stream.runForEach(stream, x => cell.set(x)) in the window scope, so quitting interrupts it and releases its subscription. The signal starts at the initial value and each emission replaces it.

import { source } from "dziry";
import { Effect, Schedule, Stream } from "effect";

const poll = Effect.promise(() => fetch("/api/notifications").then((r) => r.json()));
const live = Stream.repeatEffect(poll).pipe(Stream.schedule(Schedule.spaced("5 seconds")));

export const notifications = source<Notification[]>(() => live, []);

notifications is an ordinary ReadonlySignal<Notification[]> — bare reads, .map and bind:value all work. The explicit generic states the emission type, because dziry is type-blind to effect by design: it can recognise a Stream at run time but cannot name Stream<A> to infer A (the type parameter lives under a unique symbol only effect exports). The initial value carries the type.

The subscribe is a thunk because a stream needs the window layer's live services, which do not exist while the module is evaluated — the runtime calls it once the layer is built. source also accepts a plain callback ((set) => unsubscribe) that needs no Effect; see Signals.

Redirect and Cancel are tags a handler throws — or an Effect fails with — to drive the router. Matching is by _tag, so the same objects work in a project that has never installed effect and in one that has.

import { Redirect } from "dziry";
import { Effect } from "effect";

export const guard = () => Effect.fail(new Redirect("login")); // or: throw new Redirect("login")

Route loaders

A route object's loader may return an Effect; its success value is the component's data, and its failure is the errorComponent's error — both resolved by the generated types, so Effect<Product, DbError, Store> types data: Product and error: DbError with no manual annotation:

// pages/products/$id.tsx
import { defineRoute } from "dziry";
import { Effect } from "effect";

const route = defineRoute("products/$id")({
loader: ({ id }) => fetchProduct(id), // Effect<Product, DbError, Store>
component: Product,
errorComponent: ProductError,
});
export default route;

dziry still never imports effect itself — it recognises the returned Effect structurally and runs it through a lazy import, so a window with no Effect loader loads zero bytes of the library. A loader that fails with Redirect/Cancel navigates or stays; a superseded loader's exit is ignored.