smithery.ai

asyncredux-undo-redo

Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.

First seen Mar 19, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,764 B
  • docs SUMMARY.md 250 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Undo and Redo in AsyncRedux

AsyncRedux simplifies undo/redo through a straightforward pattern: create a state observer to save states to a history list, and create actions to navigate through that history.

Understanding StateObserver

The StateObserver abstract class tracks state modifications. Implement its observe method to be notified of state changes:

abstract class StateObserver<St> {
  void observe(
    ReduxAction<St> action,
    St stateIni,
    St stateEnd,
    Object? error,
    int dispatchCount,
  );
}

Parameters:

  • action - The dispatched action that triggered the state change
  • stateIni - The state before the reducer applied changes
  • stateEnd - The new state returned by the reducer
  • error - Null if successful; contains the thrown error otherwise
  • dispatchCount - Sequential dispatch number

Timing: The observer fires right after the reducer returns, before both the after() method and error-wrapping processes.

Step 1: Create the UndoRedoObserver

class UndoRedoObserver implements StateObserver<AppState> {
  final List<AppState> _history = [];
  int _currentIndex = -1;
  final int maxHistorySize;

  UndoRedoObserver({this.maxHistorySize = 50});

  @override
  void observe(
    ReduxAction<AppState> action,
    AppState stateIni,
    AppState stateEnd,
    Object? error,
    int dispatchCount,
  ) {
    // Skip if action had an error
    if (error != null) return;

    // Skip if state didn't change
    if (stateIni == stateEnd) return;

    // Skip undo/redo actions to prevent recursive history entries
    if (action is UndoAction || action is RedoAction) return;

    // When navigating backwards then performing new actions,
    // clear the "future" history
    if (_currentIndex < _history.length - 1) {
      _history.removeRange(_currentIndex + 1, _history.length);
    }

    // Add the new state to history
    _history.add(stateEnd);
    _currentIndex = _history.length - 1;

    // Enforce maximum history size by removing oldest entries
    while (_history.length > maxHistorySize) {
      _history.removeAt(0);
      _currentIndex--;
    }
  }

  /// Returns the previous state, or null if at the beginning
  AppState? getPreviousState() {
    if (_currentIndex > 0) {
      _currentIndex--;
      return _history[_currentIndex];
    }
    return null;
  }

  /// Returns the next state, or null if at the end
  AppState? getNextState() {
    if (_currentIndex < _history.length - 1) {
      _currentIndex++;
      return _history[_currentIndex];
    }
    return null;
  }

  bool get canUndo => _currentIndex > 0;
  bool get canRedo => _currentIndex < _history.length - 1;
}

Step 2: Register the Observer with the Store

Pass the observer to stateObservers during store creation:

// Create the observer instance so actions can access it
final undoRedoObserver = UndoRedoObserver(maxHistorySize: 100);

var store = Store<AppState>(
  initialState: AppState.initialState(),
  stateObservers: [undoRedoObserver],
);

Step 3: Create Navigation Actions

Create UndoAction and RedoAction that retrieve states from history:

class UndoAction extends ReduxAction<AppState> {
  final UndoRedoObserver observer;

  UndoAction(this.observer);

  @override
  AppState? reduce() {
    return observer.getPreviousState();
  }
}

class RedoAction extends ReduxAction<AppState> {
  final UndoRedoObserver observer;

  RedoAction(this.observer);

  @override
  AppState? reduce() {
    return observer.getNextState();
  }
}

Alternative: Access the observer through dependency injection using the environment pattern:

class UndoAction extends ReduxAction<AppState> {
  @override
  AppState? reduce() {
    final observer = env.undoRedoObserver;
    return observer.getPreviousState();
  }
}

Step 4: Integrate with the UI

Dispatch undo/redo actions from widgets:

class UndoRedoButtons extends StatelessWidget {
  final UndoRedoObserver observer;

  const UndoRedoButtons({required this.observer});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        IconButton(
          icon: Icon(Icons.undo),
          onPressed: observer.canUndo
              ? () => context.dispatch(UndoAction(observer))
              : null,
        ),
        IconButton(
          icon: Icon(Icons.redo),
          onPressed: observer.canRedo
              ? () => context.dispatch(RedoAction(observer))
              : null,
        ),
      ],
    );
  }
}

Partial State Undo/Redo

The same approach works to undo/redo only part of the state. This is useful when you want to track changes to a specific slice of state independently.

class PartialUndoRedoObserver implements StateObserver<AppState> {
  final List<DocumentState> _history = [];
  int _currentIndex = -1;
  final int maxHistorySize;

  PartialUndoRedoObserver({this.maxHistorySize = 50});

  @override
  void observe(
    ReduxAction<AppState> action,
    AppState stateIni,
    AppState stateEnd,
    Object? error,
    int dispatchCount,
  ) {
    if (error != null) return;
    if (action is UndoDocumentAction || action is RedoDocumentAction) return;

    // Only track changes to the document portion of state
    if (stateIni.document == stateEnd.document) return;

    if (_currentIndex < _history.length - 1) {
      _history.removeRange(_currentIndex + 1, _history.length);
    }

    _history.add(stateEnd.document);
    _currentIndex = _history.length - 1;

    while (_history.length > maxHistorySize) {
      _history.removeAt(0);
      _currentIndex--;
    }
  }

  DocumentState? getPreviousDocument() {
    if (_currentIndex > 0) {
      _currentIndex--;
      return _history[_currentIndex];
    }
    return null;
  }

  DocumentState? getNextDocument() {
    if (_currentIndex < _history.length - 1) {
      _currentIndex++;
      return _history[_currentIndex];
    }
    return null;
  }
}

class UndoDocumentAction extends ReduxAction<AppState> {
  final PartialUndoRedoObserver observer;

  UndoDocumentAction(this.observer);

  @override
  AppState? reduce() {
    final previousDoc = observer.getPreviousDocument();
    if (previousDoc == null) return null;
    return state.copy(document: previousDoc);
  }
}

Managing History Limits

Key considerations for history management:

  1. Set appropriate limits - Balance memory usage with undo depth needs
  2. Remove oldest entries - When exceeding the limit, remove from the beginning
  3. Clear future history - When new actions occur after undoing, discard the redo stack
  4. Filter irrelevant actions - Skip actions that don't change state or are navigation actions
// Example: Different limits for different use cases
final documentObserver = UndoRedoObserver(maxHistorySize: 100); // Heavy undo
final preferencesObserver = UndoRedoObserver(maxHistorySize: 10); // Light undo

Complete Example

// observer.dart
class UndoRedoObserver implements StateObserver<AppState> {
  final List<AppState> _history = [];
  int _currentIndex = -1;
  final int maxHistorySize;

  UndoRedoObserver({this.maxHistorySize = 50});

  @override
  void observe(
    ReduxAction<AppState> action,
    AppState stateIni,
    AppState stateEnd,
    Object? error,
    int dispatchCount,
  ) {
    if (error != null) return;
    if (stateIni == stateEnd) return;
    if (action is UndoAction || action is RedoAction) return;

    if (_currentIndex < _history.length - 1) {
      _history.removeRange(_currentIndex + 1, _history.length);
    }

    _history.add(stateEnd);
    _currentIndex = _history.length - 1;

    while (_history.length > maxHistorySize) {
      _history.removeAt(0);
      _currentIndex--;
    }
  }

  AppState? getPreviousState() {
    if (_currentIndex > 0) {
      _currentIndex--;
      return _history[_currentIndex];
    }
    return null;
  }

  AppState? getNextState() {
    if (_currentIndex < _history.length - 1) {
      _currentIndex++;
      return _history[_currentIndex];
    }
    return null;
  }

  bool get canUndo => _currentIndex > 0;
  bool get canRedo => _currentIndex < _history.length - 1;

  void clear() {
    _history.clear();
    _currentIndex = -1;
  }
}

// actions.dart
class UndoAction extends ReduxAction<AppState> {
  @override
  AppState? reduce() => env.undoRedoObserver.getPreviousState();
}

class RedoAction extends ReduxAction<AppState> {
  @override
  AppState? reduce() => env.undoRedoObserver.getNextState();
}

// main.dart
void main() {
  final undoRedoObserver = UndoRedoObserver(maxHistorySize: 100);

  final store = Store<AppState>(
    initialState: AppState.initialState(),
    stateObservers: [undoRedoObserver],
    environment: Environment(undoRedoObserver: undoRedoObserver),
  );

  runApp(
    StoreProvider<AppState>(
      store: store,
      child: MyApp(),
    ),
  );
}

References

URLs from the documentation: