Claude-skill-registry asyncredux-before-after

Implement action lifecycle methods `before()` and `after()`. Covers running precondition checks, showing/hiding modal barriers, cleanup logic in `after()`, and understanding that `after()` always runs (like a finally block).

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-before-after" ~/.claude/skills/majiayu000-claude-skill-registry-asyncredux-before-after && rm -rf "$T"
manifest: skills/data/asyncredux-before-after/SKILL.md
source content

AsyncRedux Before and After Methods

Action Lifecycle Overview

Every

ReduxAction
has three lifecycle methods that execute in order:

  1. before()
    - Runs first, before the reducer
  2. reduce()
    - The main reducer (required)
  3. after()
    - Runs last, always executes

Only

reduce()
is required. The
before()
and
after()
methods are optional hooks for managing side effects.

The before() Method

The

before()
method executes before the reducer runs. It can be synchronous or asynchronous.

Synchronous before()

class MyAction extends ReduxAction<AppState> {
  @override
  void before() {
    // Runs synchronously before reduce()
    print('Action starting');
  }

  @override
  AppState? reduce() {
    return state.copy(counter: state.counter + 1);
  }
}

Asynchronous before()

class MyAction extends ReduxAction<AppState> {
  @override
  Future<void> before() async {
    // Runs asynchronously before reduce()
    await validatePermissions();
  }

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

Precondition Checks in before()

If

before()
throws an error,
reduce()
will NOT run. This makes it ideal for validation:

class FetchUserData extends ReduxAction<AppState> {
  @override
  Future<void> before() async {
    if (!await hasInternetConnection()) {
      throw UserException('No internet connection');
    }
  }

  @override
  Future<AppState?> reduce() async {
    // Only runs if before() completed without error
    final user = await api.fetchUser();
    return state.copy(user: user);
  }
}

Common before() Use Cases

  • Validate preconditions (authentication, permissions)
  • Check network connectivity
  • Show loading indicators or modal barriers
  • Log action start for analytics
  • Dispatch prerequisite actions

The after() Method

The

after()
method executes after the reducer completes. Its key property: it always runs, even if
before()
or
reduce()
throws an error
. This makes it similar to a
finally
block.

Basic after()

class MyAction extends ReduxAction<AppState> {
  @override
  AppState? reduce() {
    return state.copy(counter: state.counter + 1);
  }

  @override
  void after() {
    // Always runs, regardless of success or failure
    print('Action completed');
  }
}

Guaranteed Cleanup

Because

after()
always runs, it's perfect for cleanup operations:

class SaveDocument extends ReduxAction<AppState> {
  @override
  Future<void> before() async {
    dispatch(ShowSavingIndicatorAction(true));
  }

  @override
  Future<AppState?> reduce() async {
    await api.saveDocument(state.document);
    return state.copy(lastSaved: DateTime.now());
  }

  @override
  void after() {
    // Hides indicator even if save fails
    dispatch(ShowSavingIndicatorAction(false));
  }
}

Important: Never Throw from after()

The

after()
method should never throw errors. Any exception thrown from
after()
will appear asynchronously in the console and cannot be caught normally:

// WRONG - Don't throw in after()
@override
void after() {
  if (someCondition) {
    throw Exception('This will cause problems');
  }
}

// CORRECT - Handle errors gracefully
@override
void after() {
  try {
    cleanup();
  } catch (e) {
    // Log but don't throw
    logger.error('Cleanup failed: $e');
  }
}

Common after() Use Cases

  • Hide loading indicators or modal barriers
  • Close database connections or file handles
  • Release temporary resources
  • Log action completion for analytics
  • Dispatch follow-up actions

Modal Barrier Pattern

A common pattern is showing a modal barrier (blocking overlay) during async operations:

class MyAction extends ReduxAction<AppState> {
  @override
  Future<AppState?> reduce() async {
    String description = await read(Uri.http("numbersapi.com", "${state.counter}"));
    return state.copy(description: description);
  }

  @override
  void before() => dispatch(BarrierAction(true));

  @override
  void after() => dispatch(BarrierAction(false));
}

The

BarrierAction
would update state to show/hide a loading overlay:

class BarrierAction extends ReduxAction<AppState> {
  final bool show;
  BarrierAction(this.show);

  @override
  AppState reduce() => state.copy(showBarrier: show);
}

Creating Reusable Mixins

For patterns you use repeatedly, create a mixin:

mixin Barrier on ReduxAction<AppState> {
  @override
  void before() {
    super.before();
    dispatch(BarrierAction(true));
  }

  @override
  void after() {
    dispatch(BarrierAction(false));
    super.after();
  }
}

Then apply it to any action:

class FetchData extends ReduxAction<AppState> with Barrier {
  @override
  Future<AppState?> reduce() async {
    // Barrier shown automatically before this runs
    final data = await api.fetchData();
    return state.copy(data: data);
    // Barrier hidden automatically after (even on error)
  }
}

Multiple Mixins

You can combine multiple mixins:

class ImportantAction extends ReduxAction<AppState> with Barrier, NonReentrant {
  @override
  Future<AppState?> reduce() async {
    // Has both modal barrier AND prevents duplicate dispatches
    return state;
  }
}

Error Handling Flow

Understanding how errors interact with the lifecycle:

class MyAction extends ReduxAction<AppState> {
  @override
  Future<void> before() async {
    // If this throws, reduce() is skipped, after() still runs
  }

  @override
  Future<AppState?> reduce() async {
    // If this throws, state is not changed, after() still runs
  }

  @override
  void after() {
    // ALWAYS runs regardless of errors above
  }
}

Checking What Completed

Use

ActionStatus
to determine which methods finished:

var status = await dispatchAndWait(MyAction());

if (status.hasFinishedMethodBefore) {
  print('before() completed');
}

if (status.hasFinishedMethodReduce) {
  print('reduce() completed');
}

if (status.hasFinishedMethodAfter) {
  print('after() completed');
}

if (status.isCompletedOk) {
  print('Both before() and reduce() completed without errors');
}

if (status.isCompletedFailed) {
  print('Error: ${status.originalError}');
}

Relationship with abortDispatch()

If

abortDispatch()
returns
true
, none of the lifecycle methods run:

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

  @override
  void before() {
    // Skipped if abortDispatch() returns true
  }

  @override
  AppState? reduce() {
    // Skipped if abortDispatch() returns true
  }

  @override
  void after() {
    // Skipped if abortDispatch() returns true
  }
}

Complete Example

class SubmitForm extends ReduxAction<AppState> {
  final String formData;
  SubmitForm(this.formData);

  @override
  Future<void> before() async {
    // Validate preconditions
    if (state.user == null) {
      throw UserException('Please log in first');
    }

    if (!await checkInternetConnection()) {
      throw UserException('No internet connection');
    }

    // Show loading state
    dispatch(SetSubmittingAction(true));
  }

  @override
  Future<AppState?> reduce() async {
    final result = await api.submitForm(formData);
    return state.copy(
      lastSubmission: result,
      submissionCount: state.submissionCount + 1,
    );
  }

  @override
  void after() {
    // Always hide loading state, even on error
    dispatch(SetSubmittingAction(false));

    // Log completion
    analytics.log('form_submitted');
  }
}

Built-in Mixins Using before() and after()

Several AsyncRedux mixins use these methods internally:

MixinUses before()Uses after()Purpose
CheckInternet
YesNoVerifies connectivity, shows dialog if offline
AbortWhenNoInternet
YesNoSilently aborts if offline
Throttle
NoYesLimits execution frequency
NonReentrant
YesYesPrevents duplicate dispatches
Retry
NoYesRetries on failure
Debounce
NoNoWaits for input pause (uses
wrapReduce
)

When using these mixins, be aware that they may already override

before()
or
after()
. Call
super.before()
and
super.after()
if you need to combine behaviors.

References

URLs from the documentation: