Skip to main content

Your first editor

An editor in is a React component tied to one . It has one job: turn user input into actions and render the document's state.

This lesson builds a minimal editor for the Todo model from the previous lesson. You'll need ph vetra running and the Todo model from the previous step already in place.

Scaffold the editor​

In Vetra Studio, open the vetra-<hash> drive, then click Document Editor to create a new editor specification. Give it:

  • Name: todo-editor
  • Document type: powerhouse/todo

Once you confirm (not just save as draft — the editor must be confirmed for codegen to run), codegen scaffolds the editor files:

editors/
└── todo-editor/
├── editor.tsx # yours to implement
├── module.ts # auto-generated, registers the editor
└── index.ts

You can also scaffold the editor manually with:

ph generate --editor todo-editor --document-type powerhouse/todo

Implement the editor​

The generated editor.tsx uses the useSelectedTodoDocument() hook to get the current document and a dispatch function. No props are passed — the hook connects to whichever Todo document is open.

editors/todo-editor/editor.tsx
import { useState } from "react";
import { useSelectedTodoDocument } from "todo-app/document-models/todo";
import {
addTodo,
completeTodo,
removeTodo,
} from "todo-app/document-models/todo";

export default function Editor() {
const [document, dispatch] = useSelectedTodoDocument();
const [title, setTitle] = useState("");

return (
<div className="todo">
<form
onSubmit={(e) => {
e.preventDefault();
if (!title.trim()) return;
dispatch(addTodo({ id: crypto.randomUUID(), title }));
setTitle("");
}}
>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs doing?"
/>
<button type="submit">Add</button>
</form>

<ul>
{document.state.global.items.map((t) => (
<li key={t.id}>
<label>
<input
type="checkbox"
checked={t.done}
onChange={() => dispatch(completeTodo({ id: t.id }))}
/>
{t.title}
</label>
<button
type="button"
onClick={() => dispatch(removeTodo({ id: t.id }))}
>
×
</button>
</li>
))}
</ul>
</div>
);
}

Notice what the editor does not do:

  • It never mutates state directly.
  • It never talks to the network.
  • It never generates timestamps or hashes — the handles that.

All the editor does is dispatch. The reducer handles the rest.

QuizThe user clicks the checkbox on a todo. What's the correct order of events?

How editors are registered​

The generated module.ts exports an EditorModule object that links the component to the document type:

editors/todo-editor/module.ts
import type { EditorModule } from "document-model";
import { lazy } from "react";

export const TodoEditor: EditorModule = {
Component: lazy(() => import("./editor.js")),
documentTypes: ["powerhouse/todo"],
config: {
id: "todo-editor",
name: "Todo Editor",
},
};

Your package's editors/editors.ts collects all EditorModule exports:

editors/editors.ts
import type { EditorModule } from "document-model";
import { TodoEditor } from "./todo-editor/module.js";

export const editors: EditorModule[] = [TodoEditor];

Powerhouse reads this file to know which UI to render for which document type.

Preview in Vetra Studio​

With ph vetra running, open the preview-<hash> drive (labelled "Vetra Preview") in the browser. Create a new Todo document there — your editor renders immediately. Any change you make to editor.tsx triggers a hot reload.

What you have now​

You've got a full vertical slice:

  • A typed document model describing state and actions.
  • A pure reducer that defines every state transition.
  • A React editor that dispatches actions and reads doc.state.global.items.

That's already enough to run offline, with replay, undo, and audit available. The next lesson turns the final key — sync.