Seed's New group button used to open a dialog with one field, while editing a group showed four: description, colour and emoji besides the name. This post is for developers and product people who keep building a create form and an edit form for the same thing, and wonder whether they should.
TL;DR
- In my apps, Add and Edit are one screen: New group opens the group's editor at /groups/new, and an existing group opens the same component at /groups/:id.
- One component means one set of fields, one check and one place for the loading, error and offline states, so create and edit cannot drift apart.
- A real address beats ?new: you can link to it and reload it, and after a create the address is replaced, so Back does not reopen an empty form.
- Ids made on the device let Seed create tags and notes offline and send them later with the same id. Groups still need a connection.
Seed: Live: Open the live demo · Code: github.com/spy4x/seed
In my apps, Add and Edit are one screen. "New group" opens the group's editor at
/groups/new in create mode, and an existing group opens the same editor at
/groups/:id: the same component, the same fields, the same checks. One screen
for add and edit, never a one-field dialog for create and another form for edit.
This post is for developers and product people who build a create form and an
edit form for the same thing. I'll show the before and after from
Seed, my SaaS starter: why one component, why a real address
beats ?new, what offline-first adds, and where Seed does not do that part yet.
What was wrong with a "New group" dialog?
On 16 October I wrote down what annoyed me in Seed's app, and it became
an issue. One item:
Always make "Add/New " button to open the entity's editor (in "create"
mode), not a dialog with only one field "name". [...] it must always be THE
SAME COMPONENT.
Here is what it was about. "New group" opened a dialog with one field (trimmed
from
the pull request that removed it):
<Modal open title="New group" cancelLabel="Close" onClose={closeCreate}> <ScreenForm pending={creating} onSubmit={onCreate}> <Field id="group-name" label="Name" error={error} required> <Input name="name" maxLength={100} value={name} required autoFocus /> </Field> {/* Cancel and "Create group" */} </ScreenForm> </Modal> Editing a group was a different thing: the name was renamed in place, and a
separate "Details" dialog held the description, colour and emoji. So a group was
born with a name only, and you had to find the second dialog to finish it. Tags,
API tokens and invitations repeated the same dialog pattern.
Nothing in it was broken. It just looked like two features that happened to
share a database table.
Why one component instead of two forms?
Because two forms for one thing drift. A new field has to be added twice. A
check has to be written twice. The loading, error and offline states have to be
drawn twice, and one of them gets forgotten. The person sees two different
screens for the same group and learns two layouts.
Seed's routes now point both addresses at one view
(app.tsx):
"/groups/new": () => <GroupEditorView key="new" />, "/groups/:groupId/:tab?": (params) => <GroupPage groupId={params.groupId} tab={params.tab} />, GroupPage renders the same GroupEditorView with the group's id, and the view
picks the mode
(GroupEditorView.tsx):
{ groupId === null ? <NewGroup /> : <ExistingGroup groupId={groupId} tab={tab} /> } Both branches draw one GroupEditorScreen. Create mode passes creating; edit
mode passes the saved group. What that buys:
- One set of fields. Name, description, emoji, colour and, since November, the group's time zone, in both modes. A create sends them all in one request.
- One check.
checkGroupDraftchecks the draft with the server's own parsers, so a typo is caught before the round trip, and bothcreate()andsave()call it (groups.ts). - One place for every state. Loading, "this group does not exist", errors under their field, saving, the offline notice. Written once, so create cannot forget one.
- Small, visible differences. Create mode says "New group" with Cancel and "Create group". Edit mode is titled with the saved name, and Discard and "Save group" show only after you change something. Leaving with unsaved changes asks first, in both.
The fields themselves come from my own
Preact component library, so
the editor is mostly wiring. The note editor already worked this way; it was the
model. Seed's UI rules now say it as rule 2,
"One editor per entity",
so the next entity an app adds starts right.
Why /groups/new and not ?new?
Before, "New group" in the group switcher went to /groups?new, and the groups
page opened the dialog when it saw new in the query. A dialog over a list is
not a place. /groups/new is: you can link to it, reload it, and leave it with
Back, and the browser's history makes sense.
The interesting moment is right after the create. The address /groups/new is
now wrong: the group exists. So Seed swaps it for the group's page instead of
pushing a new entry
(GroupEditorView.tsx):
const group = await groups.create() if (!group) return false // The group is in the list before the address changes, so its page draws at once. navigate(GROUP_PATHS.settings(group.id), { replace: true }) void selection.select(group.id) replace: true is the whole trick. Back from the new group goes to where you
were before, the groups list in my case, not to an emptied "New group" form. The
new group also becomes your current one, since you made it to work in it, and a
toast says so. No reload: the route changes inside the app, and the same editor
draws again in edit mode, now with the Members, Billing and Activity tabs.
What does offline-first add?
In an offline-first app the device makes the id, not the database. Seed gives
every new record a UUID version 7 on the client
(newId: () => uuidV7())
and sends it with the create. Since the id exists before the server answers, the
editor's address exists too.
Tags and notes go one step further: you can create them with no connection. Here
is a tag I created in the demo with the network off:
What happened there:
- "Create tag" put the create into a queue on the device, with the tag's id.
- The tag showed up in the list, and the editor moved from
/tags/newto/tags/<id>in edit mode, exactly as it does online. - When the network came back, the queue sent
tag.createwith the same id (new-ids.test.tschecks exactly this). - If the queue sends a create again because an answer got lost, the server sees that the id already holds this tag and answers with it, so a retry cannot make a copy. Only a different tag under the same id is refused ("Tag id is already in use").
One editor helps here too: the group editor's offline notice is written once,
with one sentence for creating and one for saving, and both modes show it.
Now the honest part. Groups don't do this yet. Creating a group needs the
server. With no network, the group editor lets you fill in the form and says
"You can fill this in now, but you need a connection to create the group." A
create that fails keeps what you typed. Each try at a group also gets a fresh
id, so the same-id protection covers tags and notes, not groups. The rest of
Seed's offline layer is in the Seed post.
What else did one editor drag along?
The same issue had more in it, and my AI agents built it in four pull requests
in Seed's previous repository, on 16 and 17 October (how I run them is in
my AI coding agent setup):
- Tags got
/tags/newand/tags/:tagIdfirst, with oneTagEditorScreen. - Groups got the editor above. Clicking a group in the list now opens its editor, not its notes, and the "Open notes" button is gone: notes are not the only thing an app will ever have.
- API tokens and invitations are created on pages too, at
/tokens/newand/groups/:groupId/invitations/new. They are never edited, so their pages are create-only, in two steps: the form, then the token or link shown once, with "Leave without copying?" if you try to leave first. A dialog that shows a secret once is very easy to close by accident. - The header bar went away, and the sidebar is now the app's only frame. Not about editors, but the same issue.
So the next time a "New something" button opens a dialog with one field, ask
what the edit screen looks like. If the answer is "different", you have two
forms, and they have already started drifting.
First published on antonshubin.com.








