DEV Community

Giacomo Galdzicki
Giacomo Galdzicki

Posted on Fully Autonomous

Model a multi-view JavaScript workspace around stable IDs

Disclosure: I work on Protoron, a browser workspace that connects planning, structured writing, and references. This tutorial uses a small original example to explain a general data-modeling pattern; it does not reproduce Protoron's internal implementation.

Imagine a workspace with a board and a writing view. Both show the same scene. If each view stores its own copy, changing the title in one place creates a synchronization problem.

For a small client-side model, keep one record for each entity and let views refer to its ID. The ID represents identity; the title is editable content.

Separate entities from their arrangement

The scene's title belongs to the scene. Its position on a board belongs to that board. Its position in a writing outline belongs to the outline.

Here is a minimal model:

const state = {
  scenes: new Map([
    ["scene-1", { id: "scene-1", title: "Arrival", purpose: "Introduce the conflict" }],
    ["scene-2", { id: "scene-2", title: "Departure", purpose: "Show the consequence" }],
  ]),
  board: [
    { sceneId: "scene-1", x: 80, y: 120 },
    { sceneId: "scene-2", x: 360, y: 120 },
  ],
  outline: ["scene-1", "scene-2"],
  references: [
    { id: "ref-1", sceneId: "scene-1", note: "Keep the doorway visible" },
  ],
};
Enter fullscreen mode Exit fullscreen mode

The board holds coordinates, while the outline holds order. Neither needs another scene title to maintain.

Use IDs with a suitable uniqueness scheme in an actual application. These readable IDs just keep the example easy to inspect.

Project records into each view

Selectors can assemble what the interface needs:

function selectBoard(state) {
  return state.board.flatMap((placement) => {
    const scene = state.scenes.get(placement.sceneId);
    return scene ? [{ ...placement, scene }] : [];
  });
}

function selectOutline(state) {
  return state.outline.flatMap((id) => {
    const scene = state.scenes.get(id);
    return scene ? [scene] : [];
  });
}
Enter fullscreen mode Exit fullscreen mode

These functions skip missing records rather than crashing. That is a rendering choice, not a substitute for integrity checks: validate relationships when loading data or accepting a change, and report unexpected dangling references during development.

Update the entity in one place

function renameScene(state, id, title) {
  const scene = state.scenes.get(id);
  if (!scene) throw new Error(`Unknown scene: ${id}`);

  const trimmed = title.trim();
  if (!trimmed) throw new Error("Title cannot be empty");

  const scenes = new Map(state.scenes);
  scenes.set(id, { ...scene, title: trimmed });
  return { ...state, scenes };
}
Enter fullscreen mode Exit fullscreen mode

The function copies the map and changed record. The original state remains available, which is useful when a UI depends on identity comparisons or an application wants to keep a history of states.

This alone does not cause an interface to rerender. Connect the returned state to your framework's state mechanism or your own rendering loop.

Check the relationship, not just the setter

const updated = renameScene(state, "scene-1", "Lighthouse arrival");

console.assert(selectBoard(updated)[0].scene.title === "Lighthouse arrival");
console.assert(selectOutline(updated)[0].title === "Lighthouse arrival");
console.assert(state.scenes.get("scene-1").title === "Arrival");
console.assert(updated.references[0].sceneId === "scene-1");
console.assert(updated.board[0].x === 80);
Enter fullscreen mode Exit fullscreen mode

The important behavior is that both views resolve the new title while the supporting reference and board position remain attached to the same identity.

Decide deletion behavior explicitly

Renaming is straightforward. Deleting raises a product question: should related references disappear, move to an archive, or become unassigned?

Pick that behavior deliberately and apply it as one logical change. Leaving dangling IDs behind makes a workspace difficult to reason about, even if selectors hide the missing records.

For persistence, remember that JSON does not serialize a Map's entries automatically. Convert to an array of entries or a plain object at the storage boundary, and reconstruct the map after loading. Collaborative editing also needs conflict handling and authorization; stable IDs are a useful foundation, not a complete synchronization protocol.

The pattern is small: one identity, one canonical record, and view-specific arrangement stored separately. It gives each view room to behave differently while keeping the underlying work connected.

Top comments (0)