Claude-skill-registry asyncredux-abort-dispatch

Stops an AsyncRedux (Flutter) action from dispatching. Use only when the user mentions abortDispatch(), or explicitly asks to abort or prevent dispatch under certain conditions.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/asyncredux-abort-dispatch" ~/.claude/skills/majiayu000-claude-skill-registry-asyncredux-abort-dispatch && rm -rf "$T"
manifest: skills/data/asyncredux-abort-dispatch/SKILL.md
source content

AsyncRedux Aborting the Dispatch

What is abortDispatch()?

The

abortDispatch()
method is an optional method on
ReduxAction
that lets you conditionally prevent an action from executing. When this method returns
true
, the entire action is skipped—
before()
,
reduce()
, and
after()
will NOT run, and state remains unchanged.

class MyAction extends ReduxAction<AppState> {
  @override
  bool abortDispatch() {
    // Return true to abort, false to proceed
    return someCondition;
  }

  @override
  AppState? reduce() {
    // Only runs if abortDispatch() returned false
    return state.copy(/* ... */);
  }
}

Basic Usage

The simplest use case is checking a condition before allowing the action to proceed:

class LoadUserProfile extends ReduxAction<AppState> {
  @override
  bool abortDispatch() => state.user == null;

  @override
  Future<AppState?> reduce() async {
    // Only runs if user is logged in
    final profile = await api.fetchProfile(state.user!.id);
    return state.copy(profile: profile);
  }
}

Action Lifecycle with abortDispatch()

When

abortDispatch()
returns
true
, the complete action lifecycle is skipped:

class MyAction extends ReduxAction<AppState> {
  @override
  bool abortDispatch() => state.shouldSkip;  // If true:

  @override
  void before() {
    // NOT called when aborted
  }

  @override
  AppState? reduce() {
    // NOT called when aborted
  }

  @override
  void after() {
    // NOT called when aborted
  }
}

This differs from throwing an error in

before()
, which would still cause
after()
to run.

Authentication Guard Pattern

A common pattern is creating a base action that requires authentication:

/// Base action that requires an authenticated user
abstract class AuthenticatedAction extends ReduxAction<AppState> {
  @override
  bool abortDispatch() => state.user == null;
}

/// Actions extending this will only run when user is logged in
class FetchUserOrders extends AuthenticatedAction {
  @override
  Future<AppState?> reduce() async {
    // Safe to use state.user! here - abortDispatch ensures it's not null
    final orders = await api.getOrders(state.user!.id);
    return state.copy(orders: orders);
  }
}

class UpdateUserSettings extends AuthenticatedAction {
  final Settings newSettings;
  UpdateUserSettings(this.newSettings);

  @override
  Future<AppState?> reduce() async {
    await api.updateSettings(state.user!.id, newSettings);
    return state.copy(settings: newSettings);
  }
}

Creating Base Actions with Abort Logic

You can combine multiple abort conditions in a base action:

abstract class AppAction extends ReduxAction<AppState> {
  // Override in subclasses to add action-specific abort logic
  bool shouldAbort() => false;

  @override
  bool abortDispatch() {
    // Global abort conditions
    if (state.isMaintenanceMode) return true;
    if (state.isAppLocked) return true;

    // Action-specific abort conditions
    return shouldAbort();
  }
}

class RefreshData extends AppAction {
  @override
  bool shouldAbort() {
    // Don't refresh if data is still fresh
    return state.lastRefresh != null &&
        DateTime.now().difference(state.lastRefresh!) < Duration(minutes: 5);
  }

  @override
  Future<AppState?> reduce() async {
    final data = await api.fetchData();
    return state.copy(data: data, lastRefresh: DateTime.now());
  }
}

Role-Based Authorization

Use

abortDispatch()
to implement role-based access control:

abstract class AdminAction extends ReduxAction<AppState> {
  @override
  bool abortDispatch() => state.user?.role != UserRole.admin;
}

class DeleteAllUsers extends AdminAction {
  @override
  Future<AppState?> reduce() async {
    // Only admins can reach this code
    await api.deleteAllUsers();
    return state.copy(users: []);
  }
}

Conditional Feature Actions

Prevent actions when features are disabled:

class UsePremiumFeature extends ReduxAction<AppState> {
  @override
  bool abortDispatch() => !state.user!.isPremium;

  @override
  AppState? reduce() {
    // Premium-only functionality
    return state.copy(/* ... */);
  }
}

Built-in Mixin: AbortWhenNoInternet

AsyncRedux provides

AbortWhenNoInternet
, a mixin that silently aborts actions when there's no internet connection:

class FetchLatestNews extends AppAction with AbortWhenNoInternet {
  @override
  Future<AppState?> reduce() async {
    // Only runs if internet is available
    final news = await api.fetchNews();
    return state.copy(news: news);
  }
}

Key characteristics of

AbortWhenNoInternet
:

  • No error dialogs are shown
  • No exceptions are thrown
  • The action is silently cancelled
  • Only checks if device internet is on/off (not server availability)

Compare with

CheckInternet
which shows an error dialog instead of silently aborting.

abortDispatch() vs Throwing in before()

Choose the right approach for your use case:

Approach
after()
runs?
Shows error?Use when
abortDispatch()
returns true
NoNoSilently skip action
Throw in
before()
YesYes (if UserException)Show error to user
// Silent abort - user doesn't know action was skipped
class SilentRefresh extends ReduxAction<AppState> {
  @override
  bool abortDispatch() => state.isOffline;
  // ...
}

// Visible error - user sees message
class ExplicitRefresh extends ReduxAction<AppState> {
  @override
  void before() {
    if (state.isOffline) {
      throw UserException('Cannot refresh while offline');
    }
  }
  // ...
}

When to Use abortDispatch()

Good use cases:

  • Authentication guards (action requires logged-in user)
  • Authorization checks (action requires specific role/permission)
  • Feature flags (action only for premium users)
  • Freshness checks (don't refetch if data is recent)
  • Maintenance mode (disable certain actions globally)
  • Idempotency (skip if action's effect already applied)

Consider alternatives when:

  • You want the user to see an error message (throw
    UserException
    in
    before()
    )
  • You need cleanup code to run (use
    before()
    +
    after()
    pattern)
  • You're implementing rate limiting (use
    Throttle
    or
    Debounce
    mixins)
  • You're preventing duplicate dispatches (use
    NonReentrant
    mixin)

Complete Example

// Base action with common abort logic
abstract class AppAction extends ReduxAction<AppState> {
  @override
  bool abortDispatch() {
    // Global maintenance mode check
    if (state.maintenanceMode) return true;
    return false;
  }
}

// Authenticated action that also checks maintenance mode
abstract class AuthenticatedAction extends AppAction {
  @override
  bool abortDispatch() {
    // Check parent conditions first
    if (super.abortDispatch()) return true;
    // Then check authentication
    return state.currentUser == null;
  }
}

// Admin action with full authorization chain
abstract class AdminAction extends AuthenticatedAction {
  @override
  bool abortDispatch() {
    if (super.abortDispatch()) return true;
    return state.currentUser?.role != UserRole.admin;
  }
}

// Concrete action using the hierarchy
class BanUser extends AdminAction {
  final String userId;
  BanUser(this.userId);

  @override
  Future<AppState?> reduce() async {
    // Only reaches here if:
    // 1. Not in maintenance mode
    // 2. User is logged in
    // 3. User is an admin
    await api.banUser(userId);
    return state.copy(
      users: state.users.where((u) => u.id != userId).toList(),
    );
  }
}

Important Notes

  • abortDispatch()
    is checked before
    before()
    ,
    reduce()
    , and
    after()
  • When aborted, no state changes occur
  • The action is silently skipped—no errors are thrown or logged by default
  • Use this feature judiciously; the documentation warns it's "a powerful feature" that should only be used "if you are sure it is the right solution"

References

URLs from the documentation: