Browse documentation

TUTORIAL / 03

Create a Todo

Coordinate one use case in the application layer.

A submission should reject blank text, create one item, and clear the input only after accepting it. Add RefArray to the @typed/fx imports in src/application.ts, then write the action.

Write the create action

export const createTodo = Effect.gen(function* () {
  const text = yield* TodoText;
  if (text.trim() === "") return;
  const create = yield* CreateTodo;
  // Commit a complete item before clearing the draft; a failed factory preserves input.
  yield* RefArray.prepend(TodoList, yield* create(text.trim()));
  yield* RefSubject.set(TodoText, "");
});

Read it from top to bottom. yield* TodoText gets the current draft. Whitespace returns before calling the factory. The factory receives trimmed text, and its complete result enters the list before the draft is cleared.

That order protects the user’s input: if creation fails, execution never reaches the clear. Generating the ID before insertion also gives the future row a stable key from its first render.

Run the same action for every submission

createTodo is an Effect value. Defining it does not read the draft. Each run reads current state, so a form can run this same action repeatedly without capturing yesterday’s text.

The action does not know about SubmitEvent or HTMLInputElement. In the next chapter, a native form will translate browser input into these state reads and writes.

Trace three inputs: spaces leave the list and draft alone; Learn Typed adds Learn Typed and clears the draft; an unavailable factory leaves the original text for a retry. The final chapter tests all three cases against this action.

Complete files

Keep the files from the previous step and replace or add these. Each full file is the source used by this milestone; the excerpts above select lines from it.

src/application.ts
import { Context, Effect } from "effect";
import { RefArray, RefSubject } from "@typed/fx";
import * as Domain from "./domain.js";

// Layers supply these cells, so each application or test can own fresh state.
export class TodoList extends RefSubject.Service<TodoList, Domain.TodoList>()("TodoList") {}
export class TodoText extends RefSubject.Service<TodoText, string>()("TodoText") {}
export class CreateTodo extends Context.Service<
  CreateTodo,
  (text: string) => Effect.Effect<Domain.Todo>
>()("CreateTodo") {}

export const createTodo = Effect.gen(function* () {
  const text = yield* TodoText;
  if (text.trim() === "") return;
  const create = yield* CreateTodo;
  // Commit a complete item before clearing the draft; a failed factory preserves input.
  yield* RefArray.prepend(TodoList, yield* create(text.trim()));
  yield* RefSubject.set(TodoText, "");
});
Changes from Own application state1 file
src/application.ts
BeforeAfterChangeCode
11Unchangedimport { Context, Effect } from "effect";
2Removedimport { RefSubject } from "@typed/fx";
2Addedimport { RefArray, RefSubject } from "@typed/fx";
33Unchangedimport * as Domain from "./domain.js";
44Unchanged
55Unchanged// Layers supply these cells, so each application or test can own fresh state.
… 3 unchanged lines …
99Unchanged CreateTodo,
1010Unchanged (text: string) => Effect.Effect<Domain.Todo>
1111Unchanged>()("CreateTodo") {}
12Added
13Addedexport const createTodo = Effect.gen(function* () {
14Added const text = yield* TodoText;
15Added if (text.trim() === "") return;
16Added const create = yield* CreateTodo;
17Added // Commit a complete item before clearing the draft; a failed factory preserves input.
18Added yield* RefArray.prepend(TodoList, yield* create(text.trim()));
19Added yield* RefSubject.set(TodoText, "");
20Added});