Skip to main content

Before and after the reducer

Suppose you want to prevent the user from touching the screen, while an async action is running. This means adding a modal barrier before the action starts, and removing it after the action ends.

Or suppose you want to check some precondition when an action is dispatched, and maybe throw an error if the precondition is not met.

It's indeed common to have some side effects both before and after the reducer runs. To help you with these use cases, you may override your action functions before() and after(), which run respectively before and after the reducer.

info

Implementing your action's reduce() function is mandatory, but before() and after() are optional. Their default implementation simply does nothing.

Before

The before() function runs before the reducer.

To run synchronously, return void. To run it asynchronously, add async and return Promise<void>:

// Sync
before(): void {
...
}

// Async
async before(): Promise<void> {
...
}

What happens if before() throws an error? In this case, reduce() will not run. This means you can use before() to check any preconditions, and maybe throw an error to prevent the reducer from running. For example:

// If there is no internet connection, throws a UserException 
// to show a dialog and prevent the reducer from running.
async before(): Promise<void> {
if (!(await hasInternetConnection()))
throw new UserException('No internet connection');
}
info

If before() returns a promise, then the action becomes async (its reducer will complete in a later microtask than the dispatch call), regardless of the reduce() function being sync or async.

After

Function after() runs after the reducer. It's always a synchronous function:

after(): void {
...
}

Note after() is akin to a finally block, since it will always run, even if an error was thrown by before() or reduce(). This is important so that it can undo any side effects that were done in before(), even if there was an error later in the reducer.

info

Make sure your after() function doesn't throw an error. If it does, the error will be swallowed, but logged with Store.log(). Keep in mind the default logger will print the error to the console, but you may provide your own logger to the Store constructor.

Example

Suppose we have a counter app. When you press the "Increment" button, it dispatches the Increment action, that takes 1 second to increment the counter. This action adds a dark screen barrier when it starts, and then removes the barrier when it finishes.

First, we need to create a BarrierAction:

class BarrierAction extends Action {
constructor(public hasBarrier: boolean) { super(); }

reduce() {
return this.state.copy({hasBarrier: this.hasBarrier});
}
}

And then we need a barrier component which occupies the whole screen and is shown only when hasBarrier is true:

function Barrier() {
let hasBarrier = useSelect((state: State) => state.hasBarrier);

return hasBarrier
? <div className="Barrier" />
: <></>;
}
CSS
.Barrier {
position: fixed; z-index: 9999;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.4);
}

After this is set up, you may use before() and after() to dispatch the BarrierAction:

class Increment extends Action {

async reduce() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return (state: State) => this.state.increment();
}

before() { this.dispatch(new BarrierAction(true)); }
after() { this.dispatch(new BarrierAction(false)); }
}

Creating a base action

You may also modify your base action to make it easier to add this behavior to multiple actions:

import { ReduxAction } from 'async-redux-react';
import { State } from 'State';

export abstract class Action extends ReduxAction<State> {
barrier = false;
before() { if (this.barrier) this.dispatch(new BarrierAction(true)); }
after() { if (this.barrier) this.dispatch(new BarrierAction(false)); }
}

Now you can add barrier = true; in all your desired actions, to provide before() and after() by default:

class Increment extends Action {

barrier = true;

async reduce() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return (state: State) => this.state.increment();
}
}

This is the code using the modified BaseAction and Increment actions: