Skip to content

2. Derive UI state

Goal: calculate UI state from the source of truth, and understand the yield* rule that Craft and Effect share.

Use computedEffect for synchronous Effect-backed derivations. Its factory is a generator when it reads a Craft value and returns the Effect to run:

typescript
import { Effect } from 'effect';
import { state } from '@craft-ts/core';
import { computedEffect } from '@craft-ts/effect';

const tasks = yield* state('tasks', [] as Task[]);
const remaining = computedEffect('remaining', function* () {
  const currentTasks = yield* tasks();
  return Effect.succeed(
    currentTasks.filter((task) => !task.done).length,
  );
});

The template can bind remaining directly. computedEffect runs the returned synchronous Effect in place, and Craft re-runs only the binding that depends on it.

The shared dependency vocabulary

Both runtimes use generators, but they solve different problems:

typescript
const tasks = yield* TaskList();          // Craft service
const access = yield* AccessPolicyService; // Effect service inside an Effect
const value = yield* resource.value();    // Craft reader inside a derivation

The rule is the same: yield what the current function does not own. A Craft factory yields Craft dependencies; an Effect program yields Effect dependencies. The adapter connects the two at a deliberate boundary.

Do not duplicate domain state in the component

The component should not subscribe to an Effect, convert an Effect to a signal by hand, or start a fiber in a template callback. Those approaches hide loading, cancellation and failure state from Craft. Instead:

  1. Keep the domain operation as Effect<A, E, R>.
  2. Expose it through a Craft Effect-aware primitive.
  3. Derive the display state from the resulting Craft resource.

The next step defines the domain operation and the services it requires.

What you gained

Derived state that stays reactive and a clear division: Craft derives the UI; Effect composes the domain program.