Rindle does not provide an application undo stack. You can build one by recording a command and an inverse command, then sending both through the same named mutators as ordinary edits.
This guide extends the manual synced-app quickstart.
Its example changes an issue’s status using the existing setStatus mutator.
Keep a command history
// src/history.ts
interface Command {
label: string;
do(): void;
undo(): void;
}
export function createHistory() {
const stack: Command[] = [];
let cursor = 0;
return {
run(command: Command) {
command.do();
stack.splice(cursor);
stack.push(command);
cursor++;
},
undo() {
const command = stack[cursor - 1];
if (!command) return;
command.undo();
cursor--;
},
redo() {
const command = stack[cursor];
if (!command) return;
command.do();
cursor++;
},
};
}
Capture the old and new values when the user makes an edit:
// src/status-history.ts
import { app } from "./rindle-client.ts";
import { createHistory } from "./history.ts";
export const history = createHistory();
export function changeStatus(id: string, previous: string, next: string) {
if (previous === next) return;
const set = (status: string) => {
app.mutate.setStatus({ id, status, updatedAt: Date.now() });
};
history.run({
label: "Change issue status",
do: () => set(next),
undo: () => set(previous),
});
}
Call changeStatus with the row’s current status and the requested status.
Bind history.undo() and history.redo() to your UI or keyboard handlers.
Each action generates its timestamp before invoking the mutator. The shared mutator receives that fixed argument on the browser, server, and later rebases. Do not read mutable component state from a saved command to recover its old value.
Decide what an inverse means
Undo sends a new authoritative mutation. It is not a private rewind of a view, and collaborators receive its effects. The client predicts and reconciles it like any other write.
A saved old value can overwrite a collaborator’s later edit. Rindle does not infer which edits an undo should preserve. Enforce any preconditions in your mutator and decide how your history UI handles rejections.
The stack above changes its cursor after a synchronous call succeeds. A later server rejection still needs application handling. It also does not persist history, notify React, group gestures, or limit memory use.
Deletion and external side effects need their own inverse behavior. For example, restoring a deleted row may require its old column values and related rows. For folded edits, record one command per completed gesture.