Weftv0.23.1
GitHub

Services and Async

So far our state has been self-contained. Real apps talk to services and wait on async work. Both fall out of the same fact — a Node is an Effect — so both use plain Effect.

Handlers that use services

An event handler can return an Effect, and that Effect runs in the component's environment — so it can read any service you provide at the mount boundary:

typescript
import { h } from "@weftui/core";
import { mount } from "@weftui/dom/client";
import { Context, Effect, Layer, pipe } from "effect";

class Logger extends Context.Tag("Logger")<
  Logger,
  { log: (message: string) => Effect.Effect<void> }
>() {}

const LoggerLive = Layer.succeed(Logger, {
  log: (message) => Effect.sync(() => console.log(message)),
});

const LogButton = () =>
  h.button(
    {
      onclick: () =>
        Effect.gen(function* () {
          const logger = yield* Logger;
          yield* logger.log("Button clicked");
        }),
    },
    "Log",
  );

// Provide the layer at mount — every handler in the tree can now read Logger.
void Effect.runPromise(
  pipe(mount(LogButton(), document.getElementById("root")!), Effect.provide(LoggerLive)),
);

Logger entered the tree's requirement channel the moment LogButton read it, and you discharged it once, at mount, with Effect.provide. Provide too little and it is a compile error at the mount call. This is Weft's entire dependency-injection story — it is just Effect's. The deeper treatment is Services and Context.

Async loading states

A component can return a Stream<Node> to show different content over time. Sequence a loading placeholder before the resolved content with Stream.concat:

typescript
import { h } from "@weftui/core";
import { mount } from "@weftui/dom/client";
import { Effect, Stream } from "effect";

const AsyncGreeting = ({ name }: { name: string }) =>
  Stream.concat(
    Stream.make(h.span("Loading…")),
    Stream.fromEffect(
      Effect.gen(function* () {
        yield* Effect.sleep("1 second");
        return yield* h.span(`Hello, ${name}!`);
      }),
    ),
  );

void Effect.runPromise(mount(AsyncGreeting({ name: "World" }), document.getElementById("root")!));

The stream emits the loading node first, then the resolved node — the renderer swaps the DOM in place on the second emission. This is the raw mechanism; for coordinating several async regions with a single fallback, reach for Boundary.suspend, which you will meet in the next step.

Next