Skip to content

Effect users: start here

This page is for teams that already use Effect and are evaluating CraftTS for the frontend.

The important distinction is this:

You do not need to replace your domain model or your Effect programs. You do need to adopt Craft's UI model for components, templates, reactive state, forms and routing.

Effect remains the place for domain programs, typed failures, services and Layers. Craft owns the browser-facing lifecycle: rendering, reactivity, loading, cancellation and URL state.

The boundary in one picture

mermaid
flowchart LR
  UI["Craft component and template"] --> R["Craft resource\nqueryEffect / mutationEffect"]
  R --> P["Effect program\nEffect<A, E, R>"]
  P --> L["Layer<R>"]
  L --> I["Craft injector\napplication / route / component"]
  R --> V["Reactive Craft readers\nvalue / loading / exceptions"]
  V --> UI

The two sides have different responsibilities:

ConcernEffectCraftTS
Domain rulesEffect<A, E, R>consumes the result
ServicesContext.Service + Layerprovides the Layer at a Craft scope
Business failurestagged errors in Etyped exceptions to render or handle
UI statenot the ownerstate, queryParams, derived readers
Loading and cancellationEffect runtimequeryEffect, mutationEffect, asyncProcessEffect
Components and templatesnot the ownercraftComponent and typed hyperscript

yield* appears on both sides, but it does not mean the same thing. Inside an Effect program it reads an Effect service or runs another Effect. Inside a Craft factory it declares a Craft dependency or crosses the boundary through an Effect adapter.

A 15-minute quickstart

The goal is one page that loads a user from an Effect program and renders the result through a Craft query.

1. Install the matching packages — 2 minutes

shell
npm i @craft-ts/core@beta @craft-ts/component@beta @craft-ts/effect@beta
npm i effect@rc
npm i -D @craft-ts/dev-tools@beta

Keep the Craft packages on the same version. See the compatibility and maturity matrix before using this in a production application.

2. Define the domain program — 4 minutes

This code is ordinary Effect code. It does not import Craft.

ts
import { Context, Data, Effect, Layer } from 'effect';

export type User = { readonly id: string; readonly name: string };

export class UserNotFound extends Data.TaggedError('UserNotFound')<{
  readonly userId: string;
}> {}

export type UserRepository = {
  readonly find: (userId: string) => Effect.Effect<User, UserNotFound>;
};

export class UserRepositoryService extends Context.Service<
  UserRepositoryService,
  UserRepository
>()('quickstart/UserRepository') {}

export const UserRepositoryLive = Layer.succeed(UserRepositoryService, {
  find: (userId) =>
    userId === 'user-ada'
      ? Effect.succeed({ id: userId, name: 'Ada Lovelace' })
      : Effect.fail(new UserNotFound({ userId })),
});

export const loadUser = Effect.fnUntraced(function* (userId: string) {
  const repository = yield* UserRepositoryService;
  return yield* repository.find(userId);
});

The component will call loadUser, but it will not resolve UserRepositoryService. The nearest Layer will provide it.

3. Cross the boundary with queryEffect — 4 minutes

The adapter turns Effect<User, UserNotFound, UserRepositoryService> into a Craft resource with loading, value and exception readers.

ts
import { craftComponent, p } from '@craft-ts/component';
import { queryEffect } from '@craft-ts/effect';

export const Profile = craftComponent(
  'EffectQuickstartProfile',
  {},
  function* () {
    const profile = yield* queryEffect('profile', {
      params: () => 'user-ada',
      loader: ({ params }) => loadUser(params),
    });

    return { profile };
  },
  ({ profile }) => [
    p(function* () {
      return (yield* profile.value())?.name ?? 'Loading…';
    }),
  ],
);

Do not call Effect.runPromise or subscribe inside the component. The resource owns execution, cancellation and the transition between loading, success and failure.

4. Provide the Layer and install the bridge — 3 minutes

Install the bridge once at application bootstrap. Provide the Effect Layer at the same Craft scope where the operation is used.

ts
import { provideCraftRootComponent, bootstrapCraft } from '@craft-ts/component';
import { craftAppConfig, provideAppInitializer } from '@craft-ts/core';

export const appConfig = craftAppConfig({
  routingDeps: [],
  providers: [
    provideCraftRootComponent(Profile),
    provideLayer(UserRepositoryLive),
    provideAppInitializer(() => {
      installCraftEffectBridge();
    }),
  ],
});

export const start = () => bootstrapCraft({ config: appConfig });

Run the application with your normal frontend command. The executable version of this example is also covered by the docs test suite.

5. Verify the boundary — 2 minutes

shell
npx nx test docs
npx nx typecheck demo-effect
npx nx test demo-effect

The docs test target now performs three checks: it transpiles every TypeScript or TSX code fence in learn-effect, type-checks the complete snippets under tests/snippets/learn-effect, and executes their Vitest tests. The transpilation check is intentionally syntax-focused because several excerpts are meant to be copied into an existing Craft or Effect generator; complete examples receive the stronger typecheck and runtime coverage. The Effect demo covers success, typed business errors, defects, application Layers and route-scoped Layers.

For a runnable starter that keeps this boundary intentionally small, use the repository's quickstart-effect application. It is wired into the same ESLint, EffectTS diagnostics and architecture checks that a new Effect frontend should adopt.

Which adapter should I choose?

SituationAdapter
Local toggle, draft or selectionstate
Server or domain readqueryEffect
Explicit writemutationEffect
Synchronous business calculation from an Effect servicecomputedEffect
Export, refresh or other explicit commandasyncProcessEffect
One Effect in a guard or resolverrunEffect
URL filters and pagination statenative Craft queryParams

There is intentionally no stateEffect: local UI state belongs to Craft; an Effect is introduced when a computation, I/O operation or service dependency crosses into the UI.

Continue from here