.claude/skills/asyncredux-before-after/SKILL.md
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).
npx skillsauth add marcglasberg/async_redux asyncredux-before-afterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every ReduxAction has three lifecycle methods that execute in order:
before() - Runs first, before the reducerreduce() - The main reducer (required)after() - Runs last, always executesOnly reduce() is required. The before() and after() methods are optional hooks for managing side effects.
The before() method executes before the reducer runs. It can be synchronous or asynchronous.
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);
}
}
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);
}
}
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);
}
}
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.
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');
}
}
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));
}
}
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');
}
}
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);
}
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)
}
}
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;
}
}
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
}
}
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}');
}
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
}
}
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');
}
}
Several AsyncRedux mixins use these methods internally:
| Mixin | Uses before() | Uses after() | Purpose |
|-------|--------------|--------------|---------|
| CheckInternet | Yes | No | Verifies connectivity, shows dialog if offline |
| AbortWhenNoInternet | Yes | No | Silently aborts if offline |
| Throttle | No | Yes | Limits execution frequency |
| NonReentrant | Yes | Yes | Prevents duplicate dispatches |
| Retry | No | Yes | Retries on failure |
| Debounce | No | No | Waits 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.
URLs from the documentation:
data-ai
Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.
data-ai
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
testing
Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.
tools
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.