Wrapping the reducer
You may wrap an action reducer to allow for some pre- or post-processing.
This is a complex power feature that you may not need to learn. If you do, use it with caution.
Actions allow you to define a wrapReduce()
function,
that gets a reference to the action reducer as a parameter.
If you override wrapReduce()
it's up to you to call reduce()
and
return a result.
In wrapReduce()
you may run some code before and after the reducer runs,
and then change its result, or even prevent the reducer from running.
Example
Imagine you have a chat application, where you can use the SendMsg
action
to send messages of type Msg
.
Each message has an id
, as well as a status
field that can be:
queued
: message was created in the clientsent
: message was sent to the serverreceived
: message was received by the recipient user
The action uses the service service.sendMessage()
to send the queued message,
and then updates the message status to sent
:
class SendMsg extends Action {
constructor(private msg: Msg) { super(); }
async reduce() {
await service.sendMessage(msg);
return (state: State) => this.state.setMsg(msg.id, msg.copy(status: 'sent'));
}
}
This mostly works, but there is a race condition.
The application is separately using websockets to listen to message updates from the server.
When the sent message is received by the recipient user, the websocket will let the
application know the message is now received
.
If the message status is updated to received
by the websocket before service.sendMessage(msg)
returns, the message status will be overwritten back to sent
when the action completes.
One way to fix this, is checking if the message status is already received
before updating
it to sent
. In this case, you abort the reducer.
This can be done in the reducer itself, by returning null
to abort and avoid modifying the state:
class SendMsg extends Action {
constructor(private msg: Msg) { super(); }
async reduce() {
await service.sendMessage(msg);
const currentMsg = this.state.getMsgById(msg.id);
if (currentMsg.status === 'received')
return null;
else
return (state) => this.state.setMsg(msg.id, msg.copy(status: 'sent'))
}
}
Another option is using wrapReduce()
to wrap the reducer:
class SendMsg extends Action {
constructor(private msg: Msg) { super(); }
async wrapReduce(reduce: () => ReduxReducer<St>)) {
// Get the message object before the reducer runs.
const previousMsg = this.state.getMsgById(msg.id);
const newState = await reduce();
// Get the current message object, after the reducer runs.
const currentMsg = this.state.getMsgById(msg.id);
// Only update the state if the message object hasn't changed.
return (previousMsg === currentMsg)
? newState
: null;
}
async reduce() {
await service.sendMessage(msg);
return (state) => this.state.setMsg(msg.id, msg.copy(status: 'sent'))
}
}
Creating a base action
While wrapping the reducer may seem more work, you may now modify your base action to make it easier to add this behavior to multiple actions:
export abstract class Action extends ReduxAction<State> {
observedState = undefined;
async wrapReduce(reduce: () => ReduxReducer<St>)) {
if (observedState === undefined) {
return reduce;
}
let oldObservedState = this.observedState(this.state);
let newState = await reduce();
let newObservedState = this.observedState(this.state);
return (oldObservedState === newObservedState)
? newState
: null;
}
}
Now you can easily add the observedState
function in all your desired actions,
to make sure the reducer is only applied if the observed state hasn't changed:
class SendMsg extends Action {
constructor(private msg: Msg) { super(); }
observedState = (state :State) => this.state.getMsgById(msg.id);
async reduce() {
await service.sendMessage(msg);
return (state) => this.state.setMsg(msg.id, msg.copy(status: 'sent'))
}
}