Introducing createRollbackScope: transactional state management for use-less-react
It's time to announce a powerful new addition to use-less-react that brings easy transactional semantics to your state management! Starting with version 0.13.0, you can now wrap state mutations in logic blocks that automatically rollback on failure, or explicitly rollback via an injected callback.
Meet createRollbackScope โ a utility that makes complex state operations safe and predictable.
The Problem: All-or-Nothing State Changesโ
Have you ever needed to perform multiple state mutations that should either all succeed or all fail?
One possible scenario is Optimistic updates: UI updates immediately, but the API call fails, so you must rollback the change you did optimistically.
Without transactional support, you'd need to manually track the original state and implement rollback logic yourself. Error-prone and tedious, especially if many reactive properties are involved.
The Solution: Transactional State with createRollbackScopeโ
createRollbackScope wraps any BaseOriginator or DiffOriginator instance and provides a begin() method that executes your operations within a transactional boundary:
import { createRollbackScope, rollback } from '@dxbox/use-less-react/classes';
const scope = createRollbackScope(myOriginator)
.withBatchedNotifications();
await scope.begin(async () => {
myOriginator.value = 100;
myOriginator.status = 'processing';
const result = await apiCall();
if (!result.success) {
rollback(); // Revert all changes! (manual call)
}
if (someCondition) {
throw Error("...") // Revert all changes! (auto)
}
});
If rollback() is called or any exception is thrown, all state changes made within the callback are automatically reverted.
How It Worksโ
Under the hood, createRollbackScope leverages the Memento Pattern infrastructure already present in use-less-react. It just provides syntactic sugar for making it easier and quicker to integrate.
- Before execution: A memento (state snapshot) is captured
- During execution: Notifications are handled based on the configured mode:
.withBatchedNotifications(): All notifications are batched (no intermediate UI updates).withStandardNotifications(): Notifications are sent immediately as changes occur
- On success: Changes are committed, and notifications are sent according to the configured mode
- On rollback/error: State is restored to the snapshot. If an uncaught exception caused the rollback, the error is propagated
Two Ways to Trigger a Rollbackโ
1. Explicit Rollbackโ
Call the rollback() helper when you detect a condition that should abort:
import { createRollbackScope, rollback } from '@dxbox/use-less-react/classes';
const scope = createRollbackScope(order)
.withBatchedNotifications();
await scope.begin(async () => {
order.status = 'confirmed';
order.confirmedAt = new Date();
const payment = await processPayment(order.total);
if (payment.declined) {
rollback(); // User sees order as it was at the beginning
}
});
2. Automatic Rollback on Exceptionsโ
Any exception thrown within the callback triggers an automatic rollback:
await scope.begin(async () => {
account.balance -= 500;
// If this throws, balance is automatically restored
await externalService.transfer(500);
});
Nested Rollback Scopesโ
createRollbackScope supports nested rollback scopes with intuitive semantics:
// outer scope
await scope.begin(async () => {
parent.value = 'outer change';
// inner scope
await scope.begin(async () => {
child.value = 'inner change';
rollback(); // Only inner transaction rolls back
});
parent.status = 'completed';
}); // Outer commits successfully
Key behavior:
- Inner rollbacks don't affect outer rollback scopes
- Outer rollbacks revert everything, including committed inner rollback scopes
A Basic Example: UserOriginatorโ
First, you define a class extending BaseOriginator or DiffOriginator. For this basic example we will use the former. This UserOriginator holds data about a user and defines two trivial methods getMemento and restoreMemento:
class UserOriginator extends BaseOriginator<UserState> {
constructor(
public username: string = ""
public birthDate: Date | null,
){
this.makeReactiveProperties("username", "birthDate");
}
override getMemento() {
return {
username: this.username,
birthDate: this.birthDate,
};
}
override restoreMemento(memento: UserState) {
this.username = memento.username;
this.birthDate = memento.birthDate;
}
}
From now on, you can use createRollbackScope on UserOriginator. For example, we will use it in a ViewModel:
class UserViewModel extends PubSub {
public error: string | null = null;
public isLoading: boolean = false;
constructor(public userOriginator: UserOriginator) {
super();
this.makeReactiveProperties("error")
}
async updateUsername(newUsername: string) {
const scope = createRollbackScope(this.userOriginator)
// standard notification flow (immediate) for optimistic updates
.withStandardNotifications();
this.isLoading = true;
await scope.begin(async () => {
// let UI immediately receive updated value
this.userOriginator.username = newUsername;
try {
await this.commandBus.dispatch(
new UpdateUserCommand({
username: newUsername,
})
);
} catch (err) {
// i.e. invalid username
this.error = err instanceof Error ? error.message : String(err);
rollback();
} finally {
this.isLoading = false;
}
});
}
}
Getting Startedโ
-
Update to v0.13.0:
npm install @dxbox/use-less-react@0.13.0 -
Import the utilities:
import { createRollbackScope, rollback } from '@dxbox/use-less-react/classes'; -
Create a scope and begin rollback scopes:
const scope = createRollbackScope(myOriginator)
.withBatchedNotifications();
await scope.begin(async () => {
// Your transactional operations here
});
Summaryโ
createRollbackScope brings database-style transaction semantics to your client-side state management:
- Atomic operations โ all or nothing
- Automatic rollback โ on error or explicit call
- Nested rollback scopes โ compose complex workflows
- Batched notifications โ optimal React performance
- Type-safe โ full TypeScript support
For complete API details, check out the createRollbackScope documentation.
With createRollbackScope, use-less-react continues its mission of bringing robust, enterprise-grade patterns to React applications while keeping the developer experience simple and intuitive!
