"Smart, memory-efficient state management for JS apps with built-in undo/redo. Inspired by Git and Photoshop"
-
Simple Global Key-Value Store A lightweight, globally shared state container with full type safety (when used with TypeScript).
-
Built-in Time Travel Native undo / redo support with minimal runtime and memory overhead.
-
Layered State Management Organize and isolate state changes using layers—similar to Photoshop layers—making complex state flows easier to manage.
-
Smart & Efficient History Storage
- Only changed properties are recorded—never entire objects
- Repeated updates that don’t alter state are automatically ignored
- Large objects (30+ properties) remain memory-efficient because unchanged values reference existing state
-
No Redundant History Entries Only meaningful changes create history records. Setting the same value twice produces no extra entries.
-
Tiny Footprint Under 8KB gzipped, with no unnecessary dependencies.
-
Framework-Agnostic Designed for plain JavaScript and works equally well in any environment (React, Vue, Svelte, or no framework at all).
npm install @ledgex/coreimport { Ledgex } from '@ledgex/core';
// Create a global or scoped instance
const store = new Ledgex({
bufferSize: 100 // optional, default is unlimited
});You can create:
- one global instance for the entire app
- or multiple instances for isolated state domains (e.g. editor, history, settings)
const layer = store.get('background');
store.set('background', { color: '#202020' });
store.undo();
store.redo();This example shows how Ledgex can be used to efficiently back up complex UI state in applications like photo editors, diagram tools, or design software.
- Use your own live state for immediate rendering and user feedback
- Use Ledgex only for history snapshots
- Apply throttling to avoid recording excessive intermediate states
Ledgex becomes a history engine, not your rendering bottleneck.
import throttle from 'lodash/throttle';
import { Ledgex } from '@ledgex/core';
import { layers, setLayers } from './layersStore'; // your live state
const ledgex = new Ledgex({ bufferSize: 200 });
// Throttle backups to once every 300ms
const throttledSet = throttle(ledgex.set.bind(ledgex), 300);
function handlePropsChange(layerId, newProps) {
setLayers(prevLayers =>
prevLayers.map(layer => {
if (layer.id === layerId) {
const updatedLayer = layer.clone();
updatedLayer.updateProps(newProps);
// Backup only meaningful state changes
throttledSet(layerId, updatedLayer.toObject());
return updatedLayer;
}
return layer;
})
);
}-
Efficient Memory Usage Only meaningful diffs are stored — no full snapshots during rapid updates.
-
Smooth User Experience Live updates remain instant, while undo/redo history stays compact.
-
Throttle-Friendly Dragging, sliders, and key presses don’t flood history with noise.
Ledgex allows multiple updates to be grouped into one undo/redo step.
This is ideal when several changes should be treated as one user action.
ledgex.set({
layer1: { x: 100, y: 200 },
layer2: { x: 100, y: 200 },
layer3: { x: 100, y: 200 }
}); // Single undo/redo entryUse cases:
- Aligning multiple layers
- Applying a preset
- Group transformations
Ledgex efficiently handles nested updates, storing only the deepest changes.
ledgex.set({
layer1: {
filters: {
brightness: 1.2,
contrast: 0.8
}
}
});Why it matters:
- Safe for complex structured data
- No redundant history entries
- No memory blow-up with deep objects
Ledgex gives you full control over history size using a configurable buffer.
const ledgex = new Ledgex({
bufferSize: 100 // keep last 100 meaningful changes
});- Limits the number of undo/redo steps kept in memory
- Oldest entries are automatically discarded
- Works together with diff-based storage
- Memory Safety → history never grows unbounded
- Performance → undo/redo remains fast
- Flexibility → tune buffer size per app or feature
The Ledgex class is a time-travel–enabled, layered state manager with efficient history tracking, undo/redo support, and subscription-based updates.
It is framework-agnostic and designed for applications that require precise state history, such as editors, design tools, and complex UIs.
import { Ledgex } from '@ledgex/core';new Ledgex(options?)| Name | Type | Default | Description |
|---|---|---|---|
options |
Object |
{} |
Optional configuration |
options.bufferSize |
number |
100 |
Maximum number of meaningful history steps to keep |
options.toleranceWindow |
number |
20 |
Time window for collapsing intermediate updates |
const ledger = new Ledgex({
bufferSize: 200,
toleranceWindow: 50
});- Each layer is an independent key-value state container.
- Layers can be activated, deactivated, and updated independently.
- History is tracked per layer, but undo/redo operates globally.
- Every meaningful change advances time.
- Undo and redo move backward or forward through meaningful states only.
- Repeated updates that do not change state are ignored.
Applies state updates at the current time.
- Automatically ignores non-meaningful changes
- Supports batch updates
- Creates a single undo/redo step
ledger.set({
background: { color: '#202020' },
layer1: { x: 100, y: 200 }
});| Name | Type | Description |
|---|---|---|
updates |
Object<string, Object> |
Map of layerId → partial state |
- Nested objects are merged deeply.
- Only changed properties are recorded.
- If nothing meaningful changes, no history entry is created.
Returns the current state of one or more layers.
ledger.get(); // all active layers
ledger.get(['background', 'layer1']);| Name | Type | Description |
|---|---|---|
layerIds |
string[] (optional) |
Specific layers to read |
Object<string, Object>Only active layers are included.
Moves to the previous meaningful state.
ledger.undo();Object<string, Object> | undefinedThe current state after undo, or undefined if undo is not possible.
Moves to the next meaningful state.
ledger.redo();Object<string, Object> | undefinedThe current state after redo, or undefined if redo is not possible.
Deactivates a layer at the current time.
ledger.remove('background');| Name | Type | Description |
|---|---|---|
layerId |
string |
Layer to deactivate |
- Deactivation is recorded in history
- Undo will restore the layer
Removes history entries older than minTime.
ledger.prune(50);| Name | Type | Description |
|---|---|---|
minTime |
number |
Earliest time to retain |
- Layers with no remaining state are removed
- Useful for manual memory management
Automatically prunes history based on bufferSize.
ledger.flush();- Keeps only the last
bufferSizemeaningful steps - Updates internal flush time
- Invoked automatically when history grows too large
Subscribes to state changes.
const unsubscribe = ledger.subscribe(() => {
console.log('State changed:', ledger.get());
});| Name | Type | Description |
|---|---|---|
callback |
Function |
Called after any state change |
() => voidUnsubscribe function.
- Callbacks are triggered asynchronously
- Safe to read state inside callback
Ledgex automatically flushes history when:
currentTime - lastFlushTime > bufferSize + toleranceWindow
This prevents unbounded memory growth during rapid updates.
const ledger = new Ledgex({ bufferSize: 100 });
ledger.set({
layer1: { x: 10, y: 20 }
});
ledger.set({
layer1: { x: 15 }
});
ledger.undo(); // reverts x to 10
ledger.redo(); // reapplies x = 15- Only meaningful changes are stored
- Undo/redo is deterministic
- Memory usage scales with change size, not object size
- No duplicate or empty history entries
- Photo / video editors
- Diagram & design tools
- Complex form editors
- Any application requiring efficient undo/redo
⭐ Star the repo if you find it useful! 🐞 Report issues on GitHub