Sorting saved articles should move their rows, not turn each array position into a different article.
An input being edited, a row observer, and a foreign widget all belong to the logical article. many
expresses that identity so the renderer can keep the same child work while its position changes.
This follows Native events with Effect. A fixed array of templates
is sufficient for fixed output; use many when records can be added, removed, updated, or reordered.
Give the record an identity independent of its position
For newly created records, generate an ID with @typed/id in the creation command and retain it with the record. The renderer consumes that key; it does not generate one on each update.
Use an ID that stays with the record. A title can change and an array index can refer to a different record after sorting, so neither is a reliable key for editable rows. Keys must also be unique within the current array; duplicate keys fail rather than merging records.
The example makes that distinction observable: reversing the list changes positions, while each row’s browser-owned note input should stay with its article.
import { RefSubject } from "@typed/fx";
import { html, many, component } from "@typed/template";
interface Article {
readonly id: string;
readonly title: string;
}
const ArticleRow = (article: RefSubject.RefSubject<Article>, id: string) => {
return html`<li data-article-id=${id}>
<span>${article.pipe(RefSubject.map((value) => value.title))}</span>
<label>Private note <input type="text" /></label>
</li>`;
};
export const SavedArticles = component(function* () {
const articles = yield* RefSubject.make<ReadonlyArray<Article>>([
{ id: "scope", title: "Understanding resource scopes" },
{ id: "events", title: "Native browser events" },
]);
const reverse = RefSubject.update(articles, (current) => [...current].reverse());
const rows = many(articles, (article) => article.id, ArticleRow);
return html`<section>
<button type="button" onclick=${reverse}>Reverse order</button>
<ul aria-label="Saved articles">${rows}</ul>
</section>`;
});
Read each retained item through its subject
ArticleRow receives a RefSubject<Article>, not a snapshot of the record. Its title projection
stays subscribed. Changing the title under the same ID updates that subject and its text part;
it does not reconstruct the row template.
In a generator-backed row, reading yield* article during setup and then interpolating only the resulting title would capture
the initial snapshot. That is appropriate for a setup decision but wrong for a displayed field
that should remain live. Derive live fields from the supplied subject.
Update domain records immutably. Mutating the same object in place can leave equality seeing the same retained value and skip the expected publication. Preserve unchanged object references when possible; recreating every plain object can cause unnecessary item publications. The renderer uses Effect equality when deciding whether to set a retained subject.
Follow three different list changes
Starting with scope, events, reversing the array retains both keys. Both child scopes, both
subjects, and their existing DOM remain. Unchanged item values are not republished, although the
renderer still visits the list and reconciles its concrete node order.
Changing events.title keeps the same key and publishes the new record to that row’s subject.
Only the subscribed title part needs to change. Removing scope closes that child’s scope and
removes its represented output. An observer acquired by that row is finalized even while the
remaining list continues to run.
The behavioral benefit is narrow: retained children keep their identity and lifetime. See Direct updates, local reconciliation for the cost model.
Make identity testable
Mount the example, type a note into the events row, capture that input object, and reverse the list.
Assert the same input is now in the moved row and its text remains with the correct article.
Then test a title update and a removal separately. Count a row resource’s acquisitions and finalizers
if its lifetime matters; final text order alone cannot reveal unnecessary recreation.
Node identity and browser-managed state are related but distinct. Test the fallback behavior your supported browsers require; the reconciliation deep dive has the platform-specific move contract.
For server-rendered lists, continue with Server rendering and hydration. That handoff also requires compatible initial state and serializable keys.