Skip to main content

Migration guide: v0.12.x → v0.13.0

This guide will help you migrate your codebase from use-less-react v0.12.x to v0.13.0. This release introduces the new createRollbackScope utility for transactional state management and includes important breaking changes to the Memento Pattern implementation.


Breaking Changes

1. getMemento() No Longer Takes Parameters

The getMemento() method in BaseOriginator has been simplified and no longer accepts a state parameter.

Before (v0.12.x):

class MyOriginator extends BaseOriginator<MyState> {
// ...

override getState() {
return {
field1: this.field1,
field2: this.field2,
};
}

override getMemento(state: MyState) {
return state; // or some transformation of state
}

override restoreMemento(memento: MyState) {
this.field1 = memento.field1;
this.field2 = memento.field2;
}
}

After (v0.13.0):

class MyOriginator extends BaseOriginator<MyState> {
// ...

// getState() is no longer required for BaseOriginator

override getMemento() {
return {
field1: this.field1,
field2: this.field2,
};
}

override restoreMemento(memento: MyState) {
this.field1 = memento.field1;
this.field2 = memento.field2;
}
}

Migration steps:

  1. Remove the state parameter from your getMemento() implementation
  2. Move the logic from getState() into getMemento()
  3. Remove the getState() method from your BaseOriginator implementations

2. getState() Removed from AbstractOriginatorProps

The getState() method has been removed from the AbstractOriginatorProps interface and is now only available in DiffOriginator where it's needed for diff tracking.

Impact:

  • BaseOriginator implementations no longer require getState()
  • Only DiffOriginator implementations need getState()
  • This change provides cleaner separation between Base and Diff originator hierarchies

Before (v0.12.x):

// Both BaseOriginator and DiffOriginator required getState()
class MyBaseOriginator extends BaseOriginator<State> {
override getState() { /* ... */ }
override getMemento(state: State) { /* ... */ }
override restoreMemento(memento: State) { /* ... */ }
}

class MyDiffOriginator extends DiffOriginator<State> {
override getState() { /* ... */ }
override getMemento(state: State) { /* ... */ }
override restoreMemento(memento: State) { /* ... */ }
}

After (v0.13.0):

// BaseOriginator doesn't need getState()
class MyBaseOriginator extends BaseOriginator<State> {
override getMemento() { /* ... */ }
override restoreMemento(memento: State) { /* ... */ }
}

// DiffOriginator still requires getState()
class MyDiffOriginator extends DiffOriginator<State> {
override getState() { /* ... */ }
override getMemento() { /* ... */ }
override restoreMemento(memento: State) { /* ... */ }
}

New Features

createRollbackScope - Transactional State Management

Version 0.13.0 introduces createRollbackScope, a powerful utility that brings database-style transaction semantics to your client-side state management.

Key features:

  • Atomic operations - all state changes succeed or all are rolled back
  • Automatic rollback - on exceptions or explicit rollback calls
  • Nested transactions - compose complex workflows
  • Configurable notifications - batched or standard notification modes
  • Type-safe - full TypeScript support

Basic usage:

import { createRollbackScope, rollback } from '@dxbox/use-less-react/classes';

const scope = createRollbackScope(myOriginator)
.withBatchedNotifications();

await scope.begin(async () => {
myOriginator.value = 'new value';

const result = await apiCall();

if (!result.success) {
rollback(); // Revert all changes
}
});

Notification modes:

  • .withBatchedNotifications() - Batches all notifications, single render
  • .withStandardNotifications() - Sends notifications immediately as changes occur

For complete documentation, see the createRollbackScope API reference.


Step-by-Step Migration

Step 1: Update Dependencies

Update your package.json:

npm install @dxbox/use-less-react@0.13.0

Step 2: Update BaseOriginator Implementations

For each BaseOriginator in your codebase:

  1. Remove the state parameter from getMemento():

    // Before
    override getMemento(state: MyState) {
    return state;
    }

    // After
    override getMemento() {
    return {
    field1: this.field1,
    field2: this.field2,
    };
    }
  2. Remove getState() method:

    // Before
    override getState() {
    return { field1: this.field1, field2: this.field2 };
    }

    // After
    // Just remove this method entirely
  3. Merge logic: Put your getState() logics into getMemento():

    // Before
    override getState() {
    return {
    field1: this.field1,
    field2: this.transformField(this.field2),
    };
    }

    override getMemento(state: MyState) {
    return state;
    }

    // After
    override getMemento() {
    return {
    field1: this.field1,
    field2: this.transformField(this.field2),
    };
    }

Step 3: Verify DiffOriginator Implementations

For DiffOriginator implementations, ensure they still have getState():

class MyDiffOriginator extends DiffOriginator<State> {
override getState() {
return { /* current state */ };
}

override getMemento() { // No state parameter
return { /* memento data */ };
}

override restoreMemento(memento: State) {
// Restore logic
}
}

Step 4: Test Your Implementation

Run your tests to ensure:

  1. All originator implementations compile without errors
  2. State snapshots are created correctly
  3. Undo/redo functionality still works as expected
  4. Memento restoration works properly

Common Migration Patterns

Pattern 1: Simple State Capture

Before:

class CounterOriginator extends BaseOriginator<{ count: number }> {
private _count = 0;

get count() { return this._count; }
set count(v: number) {
this._count = v;
this.notify('count');
}

override getState() {
return { count: this._count };
}

override getMemento(state: { count: number }) {
return state;
}

override restoreMemento(memento: { count: number }) {
this._count = memento.count;
}
}

After:

class CounterOriginator extends BaseOriginator<{ count: number }> {
private _count = 0;

get count() { return this._count; }
set count(v: number) {
this._count = v;
this.notify('count');
}

override getMemento() {
return { count: this._count };
}

override restoreMemento(memento: { count: number }) {
this._count = memento.count;
}
}

Pattern 2: Complex State with Transformation

Before:

class FormOriginator extends BaseOriginator<FormState> {
private _data: FormData = {};
private _errors: Errors = {};

override getState() {
return {
data: { ...this._data },
errors: { ...this._errors },
timestamp: Date.now(),
};
}

override getMemento(state: FormState) {
// Only save data, not errors or timestamp
return { data: state.data };
}

override restoreMemento(memento: { data: FormData }) {
this._data = { ...memento.data };
}
}

After:

class FormOriginator extends BaseOriginator<FormState> {
private _data: FormData = {};
private _errors: Errors = {};

override getMemento() {
// Only save data, not errors or timestamp
return { data: { ...this._data } };
}

override restoreMemento(memento: { data: FormData }) {
this._data = { ...memento.data };
}
}

Troubleshooting

TypeScript Errors

Error: "Property 'getState' does not exist on type 'BaseOriginator'"

Solution: Remove calls to getState() on BaseOriginator instances. If you need to access the current state, access the properties directly or create a custom getter method.

Error: "Expected 0 arguments, but got 1" on getMemento()

Solution: Remove the state parameter from your getMemento() implementation.

Runtime Errors

Issue: Memento is null or contains incorrect data

Solution: Ensure your getMemento() implementation correctly captures all necessary state directly from instance properties, not from a state parameter.

Issue: State not restoring correctly after undo

Solution: Verify that getMemento() captures the same fields that restoreMemento() expects.


Need Help?

If you encounter issues during migration:

  1. Check the createRollbackScope documentation
  2. Review the Memento Pattern documentation
  3. Open an issue on GitHub