Skip to main content

Overview

The trackHistory function creates a history log of all changes to an observable, recording the previous values with timestamps. This is useful for debugging, audit trails, or implementing custom undo/redo functionality.

Signature

function trackHistory<T>(
    value$: ObservableParam<T>,
    targetObservable?: ObservableParam<Record<TimestampAsString, Partial<T>>>
): ObservableParam<Record<TimestampAsString, any>>
value$
ObservableParam<T>
required
The observable to track changes for
targetObservable
ObservableParam<Record<TimestampAsString, Partial<T>>>
Optional observable to store the history. If not provided, a new observable is created.
Returns: An observable containing a record of changes keyed by timestamp strings.

Basic Usage

import { observable } from '@legendapp/state';
import { trackHistory } from '@legendapp/state/helpers/trackHistory';

const user$ = observable({ name: 'Alice', role: 'user' });
const history$ = trackHistory(user$);

// Make some changes
user$.set({ name: 'Bob', role: 'admin' });

// Access history
const historyData = history$.get();
// {
//   '1678901234567': { name: 'Alice', role: 'user' }
// }

Multiple Changes

Each change is recorded with a new timestamp entry containing the previous values:
import { observable } from '@legendapp/state';
import { trackHistory } from '@legendapp/state/helpers/trackHistory';

const settings$ = observable({ 
    theme: 'light', 
    lang: 'en',
    notifications: true 
});

const history$ = trackHistory(settings$);

// Change 1
settings$.theme.set('dark');

// Wait a moment...
await new Promise(resolve => setTimeout(resolve, 10));

// Change 2
settings$.lang.set('fr');

// History now contains two entries
const historyData = history$.get();
// {
//   '1678901234567': { theme: 'light' },
//   '1678901234577': { lang: 'en' }
// }

Batched Changes

When multiple changes happen in a batch, they are recorded together with a single timestamp:
import { observable, beginBatch, endBatch } from '@legendapp/state';
import { trackHistory } from '@legendapp/state/helpers/trackHistory';

const profile$ = observable({ 
    firstName: 'John', 
    lastName: 'Doe',
    email: '[email protected]'
});

const history$ = trackHistory(profile$);

// Batch multiple changes
beginBatch();
profile$.firstName.set('Jane');
profile$.email.set('[email protected]');
endBatch();

// History contains one entry with both previous values
const historyData = history$.get();
// {
//   '1678901234567': { 
//     firstName: 'John', 
//     email: '[email protected]' 
//   }
// }

Custom Target Observable

You can provide your own observable to store the history, which is useful when you want to persist the history or share it:
import { observable } from '@legendapp/state';
import { trackHistory } from '@legendapp/state/helpers/trackHistory';
import { persistObservable } from '@legendapp/state/persist';

const data$ = observable({ count: 0 });
const customHistory$ = observable({});

// Use custom observable for history
trackHistory(data$, customHistory$);

// Optionally persist the history
persistObservable(customHistory$, {
    local: 'dataHistory'
});

// Make changes - they'll be tracked in customHistory$
data$.count.set(1);
data$.count.set(2);

Remote Changes

History tracking automatically skips changes that come from persistence or sync to avoid duplicate tracking:
import { observable } from '@legendapp/state';
import { trackHistory } from '@legendapp/state/helpers/trackHistory';
import { persistObservable } from '@legendapp/state/persist';

const state$ = observable({ value: 0 });
const history$ = trackHistory(state$);

// Persist the state
persistObservable(state$, {
    local: 'state'
});

// Local changes are tracked
state$.value.set(1); // Tracked

// Changes from persistence/sync are NOT tracked
// (they already have history on the remote client)

Accessing History

The history observable is a record keyed by timestamp strings:
const history$ = trackHistory(observable({ x: 1, y: 2 }));

// Get all history entries
const allHistory = history$.get();

// Get history keys (timestamps) in order
const timestamps = Object.keys(allHistory).sort();

// Access a specific entry
const firstChange = allHistory[timestamps[0]];

// Observe history changes
history$.onChange(({ value }) => {
    console.log('History updated:', value);
});

Use Cases

Debugging

Track all changes during development:
if (process.env.NODE_ENV === 'development') {
    const history$ = trackHistory(appState$);
    
    // Log history on demand
    window.showHistory = () => {
        console.table(history$.get());
    };
}

Audit Trail

Keep a log of user actions:
const document$ = observable({ title: '', content: '' });
const auditLog$ = trackHistory(document$);

// Persist the audit log
persistObservable(auditLog$, {
    local: 'documentAudit'
});

Custom Undo/Redo

Build custom undo/redo logic using the history:
const state$ = observable({ value: 0 });
const history$ = trackHistory(state$);

function undo() {
    const historyData = history$.get();
    const timestamps = Object.keys(historyData).sort();
    
    if (timestamps.length > 0) {
        const lastTimestamp = timestamps[timestamps.length - 1];
        const previousValue = historyData[lastTimestamp];
        
        // Restore previous value
        state$.assign(previousValue);
        
        // Remove from history
        delete historyData[lastTimestamp];
        history$.set(historyData);
    }
}
For built-in undo/redo functionality, use the undoRedo helper instead.
  • undoRedo - Built-in undo/redo functionality
  • onChange - Listen to observable changes

Build docs developers (and LLMs) love