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
| Before | After | Change | Code |
|---|---|---|---|
| 1 | 1 | Unchanged | import { Context, Effect } from "effect"; |
| 2 | Removed | | |
| 2 | Added | import { RefArray, RefSubject } from "@typed/fx"; | |
| 3 | 3 | Unchanged | import * as Domain from "./domain.js"; |
| 4 | 4 | Unchanged | |
| 5 | 5 | Unchanged | // Layers supply these cells, so each application or test can own fresh state. |
| … 3 unchanged lines … | |||
| 9 | 9 | Unchanged | CreateTodo, |
| 10 | 10 | Unchanged | (text: string) => Effect.Effect<Domain.Todo> |
| 11 | 11 | Unchanged | >()("CreateTodo") {} |
| 12 | Added | | |
| 13 | Added | export const createTodo = Effect.gen(function* () { | |
| 14 | Added | const text = yield* TodoText; | |
| 15 | Added | if (text.trim() === "") return; | |
| 16 | Added | const create = yield* CreateTodo; | |
| 17 | Added | // Commit a complete item before clearing the draft; a failed factory preserves input. | |
| 18 | Added | yield* RefArray.prepend(TodoList, yield* create(text.trim())); | |
| 19 | Added | yield* RefSubject.set(TodoText, ""); | |
| 20 | Added | }); | |