01 / The idea
The object that owns the state should know how to restore it.
A rename dialog can remember one old title and put it back on Cancel. That is a reasonable solution when one string is all that changes.
Then the dialog becomes a redesign flow. It changes the title, the number of columns, and several content blocks. If the history panel copies and restores those fields itself, it must learn the editor’s internal layout. Add another field and there are now two places that need to understand a valid document.
Memento lets an object capture restorable state and later recover it, while another object keeps the checkpoint without depending on its internal representation. The editor makes the snapshot. History retains it and returns it when asked. The editor performs the restoration.
The traditional names describe those jobs: the editor is the originator, the saved state is the memento, and the history holder is the caretaker. Our caretaker stores an opaque token it cannot read, together with a label the user can recognize. Whether that token refers to the saved state or contains it is a language decision; the notes on each language below say which.
02 / See the shape
History keeps the handle. The editor knows the contents.
The Basic form saves one title behind a checkpoint and verifies that it belongs to the restoring editor. It shows the handoff without nested mutable state or a history collection.
In the wild is a newsletter editor. Its title, column layout, and two text blocks form the restorable document. A history holder keeps the latest few checkpoints. The connection and cached text preview are runtime state with different rules.
| State | On capture | On restore |
|---|---|---|
| Title, columns, blocks | Keep an independent saved version. | Make a fresh editable copy of that version. |
| Document identity | Bind the token to this editor instance. | Restore into the same editor; keep its identity. |
| Connection status | Leave it outside the checkpoint. | Keep the current status. |
| Cached preview | Leave it outside the checkpoint. | Clear it so it can be rendered again. |
One old title becomes an opaque checkpoint. The editor can restore it; a caller can retain and return it without learning the saved value.
export class BasicEditor {
title = 'Release notes';
#saved = new WeakMap<object, string>();
capture(): object {
const token = Object.freeze({});
this.#saved.set(token, this.title);
return token;
}
restore(token: object): void {
const title = this.#saved.get(token);
if (title === undefined) throw new Error('foreign-checkpoint');
this.title = title;
}
} type BasicCheckpoint struct {
owner *ownerTag
title string
}
type BasicEditor struct {
Title string
owner *ownerTag
}
func NewBasicEditor() *BasicEditor { return &BasicEditor{"Release notes", &ownerTag{}} }
func (e *BasicEditor) Capture() BasicCheckpoint { return BasicCheckpoint{e.owner, e.Title} }
func (e *BasicEditor) Restore(saved BasicCheckpoint) error {
if saved.owner == nil || saved.owner != e.owner {
return errors.New("foreign-checkpoint")
}
e.Title = saved.title
return nil
} At the call site, “Before redesign” captures Release notes, one column, and Welcome. After a redesign, the connection goes offline and a preview is rendered. Restoring the checkpoint brings back the original content, leaves the connection offline, and clears the preview. The checkpoint remains available for another restore.
The shared contract and its boundariesValidation, membership, and retention
Titles, first-block text, and checkpoint labels must be nonempty. They are preserved exactly, including spaces and Unicode; the model does not trim them or impose a text-length policy. Columns must be finite whole numbers from 1 to 3. The editor checks the title, columns, then block text before changing any field. An invalid edit leaves the preview intact. Every accepted edit clears it, even if the new values equal the current ones.
Capture has no effect on the live content or preview. Restore validates the checkpoint’s owner before changing state. Two editors with the same document ID still have different checkpoint membership. A missing history entry or foreign checkpoint leaves the document and history unchanged.
History keeps between one and five checkpoints, oldest first. Save appends a checkpoint and releases the oldest entry if needed. Restore neither consumes the checkpoint nor deletes later entries. Duplicate labels are allowed because entries are distinct. Clear releases the collection without altering the document. These are named checkpoints, rather than a linear undo/redo history.
Reading the TypeScriptPrivate state behind an empty token
Each editor has a runtime-private #saved WeakMap. A frozen empty object acts
as the checkpoint key. The branded TypeScript type discourages accidental substitution, while
membership in this editor’s map performs the actual runtime check. The token contains no public
document fields.
copyState copies the layout object, the blocks array, and every block record.
It runs at capture and again at restore. Those records contain only a string and a boolean
today. The deliberately shallow capture skips that separation so the browser can execute the
mistake being discussed.
A WeakMap does not keep its keys alive. History’s entries do retain their tokens; removing an entry can make its saved state collectible when no other holder retains the token. Collection timing is unspecified. A token cannot be serialized to recover the map’s private payload.
Reading the GoA package boundary and copied slice storage
A checkpoint wraps private data containing an owner tag and saved state. Unexported
fields protect the representation across a package boundary. The copyable file places
everything in package main; its caretaker follows that boundary by
convention. A library should put the editor and checkpoint in their own package when it
needs the compiler to enforce the separation from callers.
The layout struct copies by value, but the block slice needs a new backing array. Its string and boolean elements then copy independently. The owner tag has nonzero size so pointer identity does not rely on the special rules for zero-sized allocations. Restore checks membership and copies the saved content before exposing it to future edits.
History zeroes an evicted entry before shrinking the slice, releasing its token reference from the retained backing array. Inspection returns copies of the blocks, labels, and optional preview value.
Reading the PythonDataclasses and defensive copies
Python models the document with mutable dataclasses and gives each editor an owner
object. copy_state reconstructs the nested dataclasses at capture, restore, and view.
The checkpoint payload uses underscore-prefixed fields: Python makes that an API convention
rather than a compiler-enforced privacy boundary.
The history object stores only the checkpoint reference and label, evicts the oldest
entry with pop(0), and returns a new label list. The editor keeps the owner check and
the second copy, so a later edit cannot rewrite a retained checkpoint.
03 / Follow the values
A saved reference can quietly become the present.
Save Original, then choose Try redesign. Take the simulated connection offline and render a preview. Before restoring Original, predict which content fields should return and which runtime fields should stay current.
With shallow capture, the old title comes back but the layout and blocks remain changed. Switch to Independent memento, which resets the document, and repeat. Then edit after a restore and restore the same checkpoint again: the saved version should still be intact.
Keep a way back before trying a change.
Save Original, try the redesign, take the simulated connection offline, and render a preview. Predict what restoring Original should change.
Changing either setting starts a fresh document. This limit counts checkpoints, not bytes.
Current document · originator
newsletter-a
Release notes
1 column
Welcome
PlainChangelog
EmphasizedOutside the checkpoint
Connection: online
Preview: not rendered
Retained checkpoints · caretaker
0 / 3 retained
No checkpoint yet. Capture the state you want to return to.
History stores labels and tokens. Only the editor understands the saved document fields.
Save a checkpoint before trying the redesign.
Try a checkpoint from another editor
This creates a second editor with the same document ID and tries its checkpoint here. Membership belongs to the editor instance, not to matching ID text.
The deliberate shallow-capture mistakeTypeScript comparison only
captureShallowForComparison(): Checkpoint {
const token = Object.freeze({}) as Checkpoint;
this.#saved.set(token, { ...this.#state });
return token; // Wrong for mutable nested data: layout and blocks remain shared.
} The spread makes a new outer record, but its layout and blocks still refer to the live objects. Updating the title assigns a separate top-level string. Updating the columns or a block mutates objects the checkpoint still shares. A later deep copy during Restore can only copy the state that remains; it cannot recover the values already overwritten.
Copying only during Capture has a different failure. If Restore installs the checkpoint’s mutable objects directly, the next edit can corrupt the saved version. Independent working state is needed on both sides of the boundary. Immutable state can satisfy that requirement through a retained version instead.
In Go, plain assignment would copy the value-only layout but share the slice storage; the implementations keep their nested state independent.
04 / Try a decision
The second restoration is part of the promise.
A checkpoint may be retained while several working versions come and go. Trace the references after Restore as carefully as you traced them at Capture.
Reason through the alternatives
Copy on capture, then install the saved mutable objects directly on restore. Capture protects the checkpoint from the first round of edits. Directly installing those saved objects makes the next edit mutate the checkpoint itself. A second restore can no longer recover the original values.
Let the editor isolate saved mutable state at capture and again at restore. The editor keeps the checkpoint independent from each editable working state. History needs only the opaque token and metadata. Restoring it again returns the same content; runtime connection status stays current and the cached preview is cleared.
Give history every internal field and have it reconstruct the editor. This can recreate the values today, but history now knows the editor’s layout and block schema. A new field creates another place that must understand restoration. The Memento boundary keeps that knowledge with the object whose state is being restored.
Which fields are restorable content? Which are current runtime state? Who owns capture, restore, and the decision to keep or release a checkpoint?
This note is local to the page. It is not saved or automatically assessed.05 / Give it a real job
The edit session chooses when. The editor chooses what.
When a local redesign dialog opens, its controller asks the editor for a checkpoint. Preview controls apply edits through the editor. Cancel restores the checkpoint; Apply accepts the current content and releases that temporary checkpoint. Our lab retains named checkpoints so you can compare several versions, but the same capture/restore responsibility works with a single dialog-owned token.
The caretaker chooses retention and ordering. It can display “Before redesign” without knowing how columns or blocks are represented. Now add a nested style range to each block. The editor’s state, copy rule, and tests must change. History’s Save and Restore operations still hand around the same checkpoint type.
Capture before the operation you want to cancel. Capturing when Cancel is clicked only remembers the already changed document. If a multi-step edit can fail halfway through, decide whether the controller must restore on that failure too, or whether the editor can prepare and validate a complete replacement before committing it.
Build UIs?Every SvelteKit page snapshot is a checkpoint the router keeps for you. Dragging a card on a task board is where you choose what to capture.
Where it already is in your components
You probably already follow this rule if a SvelteKit page keeps a half-written draft:
whatever the page’s snapshot captures has to be plain JSON. Capture a Set of chosen
recipients and Back still brings it back, but reload the page and the restore breaks. The reason
is that SvelteKit runs a Memento for you.
A +page.svelte can export a snapshot with capture and restore. The page is the originator: it decides that the reply text and
the chosen recipients belong in the checkpoint. SvelteKit is the caretaker. Just before
you navigate away, it calls capture and files the value under that history
entry without looking inside. When you return with Back or Forward, it hands the value to restore. Following a link to the same page makes a new history entry, so that
visit starts empty.
The caretaker keeps the checkpoint in two forms. During the visit it holds the object in
memory, and in our Chromium run Back handed restore the very object capture had returned. When the tab is hidden or unloads, SvelteKit 2.70.3 writes every snapshot to sessionStorage with JSON.stringify, and a reload parses them back. That copy is why the docs
say the data “must be serializable as JSON.” After a reload, a captured Set came back as {}, a Date as a string, and an undefined field not at all, and the
restore loop threw “TypeError: saved.cc is not iterable.” So the page turns its state into
plain data in capture and rebuilds its own types in restore, just as our editor copies on both sides of a checkpoint.
<script lang="ts">
import type { Snapshot } from '@sveltejs/kit';
import { SvelteSet } from 'svelte/reactivity';
const teammates = ['Ana', 'Kofi', 'Mei'];
let reply = $state('');
const cc = new SvelteSet<string>();
// Back hands restore the object capture returned. A reload reads it back from
// sessionStorage as JSON, where a Set becomes {}, so capture keeps plain data.
export const snapshot: Snapshot<{ reply: string; cc: string[] }> = {
capture: () => ({ reply, cc: [...cc] }),
restore: (saved) => {
reply = saved.reply;
cc.clear();
for (const name of saved.cc) cc.add(name);
}
};
</script>
<textarea bind:value={reply} aria-label="Reply"></textarea>
{#each teammates as name (name)}
<label>
<input
type="checkbox"
checked={cc.has(name)}
onchange={(event) => (event.currentTarget.checked ? cc.add(name) : cc.delete(name))}
/>
Copy {name}
</label>
{/each}
When you have to own it
Now you’re building a task board. Dragging a card to another column moves it on screen at once and sends the change to the server. If the server refuses, the card has to go back. That is a checkpoint you own: capture before the optimistic move, restore when the request fails.
In React the capture looks free. The React tutorial names the benefit of replacing state instead of changing it: “Avoiding direct data mutation
lets you keep previous versions of the data intact, and reuse them later.” So const before = board is a snapshot only while every update copies. When a
move used splice and push on the current arrays, our rollback
left the card where the failed request had put it. In Svelte the board is a deep $state proxy that you do change in place, so const before = board follows every
later edit, and $state.snapshot(board) is the copy that stays put.
The harder decision is what the checkpoint holds. Drag card A, then card B while A’s request is still pending. If B’s request succeeds and A’s fails, restoring the whole board from before A’s move sends B back too, although the server accepted it. In our React run, whole-board rollback put both cards back in To do, while a checkpoint holding only A’s column and position returned A and left B where it was. Capture what this one move changed, and restore it onto the current board with a functional update.
Where local restoration stopsPersistence, side effects, and session lifetime
A checkpoint here is valid only for its creating editor instance. Reopening the same persisted document creates a new owner. Long-lived checkpoints need an explicit serialized schema, version compatibility, validation, and migration rules; a runtime token is not a durable format.
A request already in flight keeps running after a restore; its completion may need a generation or version check before updating the view.
Clear history when the document session ends. The retention count bounds only this caretaker’s entries; another caller can still retain a captured token. Releasing a reference does not guarantee reclamation, let alone secure erasure.
06 / Already in your toolbox
Look for the state boundary in a familiar API.
Each of these APIs draws its own line around what gets saved and who can read it.
Canvas · save and restore
The drawing context retains and restores drawing state such as transforms, clipping, and styles. The bitmap is outside that state, and the current path is separate too. Restoring a drawing state does not erase pixels you drew. It is a concrete reminder to name what a snapshot includes.
Read the canvas state contract ↗CodeMirror · immutable editor state
CodeMirror’s state and document structures are immutable; updates produce new states. An old state stays a version that later edits cannot mutate. That state is publicly readable, so the protection comes from immutability rather than an opaque token.
Read the state model ↗.NET · IEditableObject
The interface names BeginEdit, CancelEdit, and EndEdit. Cancel discards changes since editing began. That gives application controls a restoration boundary while the object owns its editing behavior, including how it remembers the earlier values.
Read the edit-session contract ↗07 / When it earns its place
Choose the representation around the work you need to preserve.
Memento helps when restoration involves internal state that callers should not reconstruct: editor checkpoints, reversible local operations, or a model with an explicit Cancel boundary. Keep a direct draft or a few old scalar values when those already express the whole requirement clearly.
This example copies all document content at Capture and Restore. Work grows with the content being copied, and retained copies consume space. For a large editor or frequent checkpoints, consider immutable versions with structural sharing, grouped changes, deltas, or periodic snapshots. Measure the workload you actually have.
Inverse operations can be smaller and more precise when an edit has a reliable inverse. A full checkpoint is convenient when many internal fields change together. Commands can retain mementos for their undo behavior, but Memento alone does not decide gesture grouping, redo branching, or how to merge concurrent edits.
Opacity is an interface decisionMetadata, immutable values, and security
Useful checkpoint metadata can be public: a label, creation time, or a version identifier. That does not require exposing every restorable field. The boundary matters when it keeps consumers from depending on the editor’s internal schema.
A publicly readable immutable value can also be a good snapshot. Encapsulation limits accidental coupling; whether a user may restore a document is a separate authorization check.
08 / Take the idea with you
Say who remembers, who keeps, and who restores.
Try explaining the design without its name: “The editor captures enough content to return to it later. History retains a labeled handle. When the user selects it, the editor restores the content while keeping current runtime state outside that boundary.” Then name a future field that would make your restoration rule incomplete.
Connections to follow nextRelated lessons
Prototype creates another instance from existing state. This Memento example restores an earlier state into the same editor. Their copy mechanics can overlap while their identity and lifecycle promises differ.
Command represents an action. It can retain a memento captured before execution when restoring state is a suitable way to undo that action. Stack supplies one possible history ordering; the editor still owns what a checkpoint means.
Copying, identity & equality explains why an equal-looking value can still share mutable state. Memento makes that distinction part of a restoration contract.