Back to Blogs
State Management

How one store coordinates active canvas state, multi-canvas tabs, and persistence

Designing a centralized state store that manages tab transitions, historical undo/redo stacks, and debounces database writes.

01Managing high-frequency interactions without database bottlenecks

An architecture editor must feel responsive. When a user drags a node across the canvas, the coordinate updates must render smoothly at 60 FPS. However, saving these coordinates directly to a database on every frame will cause thousands of network requests, leading to rate limiting, database lockups, and UI stutter. We need a centralized, in-memory client state store to handle fast interactions and update the database only when necessary.

02The centralized store architecture

We use a centralized state store (like Zustand) to act as the single source of truth. The store maintains the state of the active canvas, the list of open tabs, and user selections. When a user switches tabs, the store automatically saves the active canvas state to the tab index before loading the next canvas data, preventing data loss. This keeps the application snappy and responsive, even when working with complex multi-canvas projects.

03Implementing historical stacks for undo and redo

A reliable design tool needs robust history management. We implement this using two stacks: a Past stack and a Future stack. Every time a user performs an action (e.g., adding a node, deleting an edge, or finishing a drag), we push a snapshot of the current canvas onto the Past stack. When the user hits Undo, we pop the top state from the Past stack, apply it to the canvas, and push the previous state onto the Future stack, allowing users to move backward and forward through their edit history.

04Debouncing database saves to optimize performance

To keep the database sync efficient, we implement a debouncing queue. When a change occurs (like dragging a node), the editor updates the local in-memory store instantly. At the same time, it schedules a save function to run after a delay (e.g., 1000ms). If another edit occurs before the timer fires, the previous timer is cancelled and a new one starts. Once the user stops editing, the final state is flushed to the database in a single network request, reducing database load.