Our Redux Anti Pattern
A guide to predictable scalability
Yazan Alaboudi
@YazanAlaboudi
1 month into a project
1 year into a project
It's a scalability problem
Let's take a page from the backend playbook
Example:
Buying An Insurance Policy
Business Requirement
When a user successfully subscribes to a policy:
- Bill the user every month
Policy Service
HTTP
{ ... }
Billing Service
API call
Business Requirement
When a user successfully subscribes to a policy:
- Bill the user every month
- Send a confirmation email
Policy Service
HTTP
{ ... }
Billing Service
Email Service
API call
API call
!
requires change & redeployment
!
Business Requirement
When a user successfully subscribes to a policy:
- Bill the user every month
- Send a confirmation email
- Send an appreciation letter in the mail
Policy Service
HTTP
{ ... }
Billing Service
Email Service
Parcel Service
API call
API call
API call
!
requires change & redeployment
!
Business Requirement
When a user successfully subscribes to a policy:
- Bill the user every month
- Send a confirmation email
- Send a "thank you" letter in the mail
.... But as MORE requirements come in
Policy Service
HTTP
{ ... }
Billing Service
Email Service
Parcel Service
API call
API call
API call
!
requires change & redeployment
!
API call
API call
API call
🔥 crazy stuff is going on here
🔥
That Familiar Feeling...
Pub/Sub To the Rescue
Policy Service
Message
Bus
HTTP
{ ... }
dispatch
"Policy Purchased"
Billing Service
Email Service
Parcel Service
subscribed to
"Policy Purchased"
subscribed to
"Policy Purchased"
subscribed to
"Policy Purchased"
Publisher reports an event
Subscriber is interested in an event
Message Bus facilitates this communication
Pub/Sub Components
Policy Service
Message
Bus
HTTP
{ ... }
dispatch
"Policy Purchased"
Billing Service
Email Service
Parcel Service
subscribed to
"Policy Purchased"
subscribed to
"Policy Purchased"
subscribed to
"Policy Purchased"
publisher
bus
subscribers
1. Publishers should be unaware of their subscribers
2. Subscribers should be unaware of publishers
Pub/Sub Principle
dispatcher
redux
Actor
Action
reducer B
reducer A
reducer C
publisher
bus
subscribers
Action
Action
Action
Redux is a synchronous pub/sub system
what NOT to do
Policy Service
Message
Bus
HTTP
{ ... }
dispatch
"start billing"
Billing Service
Email Service
Parcel Service
subscribed to
"send confirmation"
subscribed to
"start billing"
subscribed to
"send letter"
dispatch
"send letter"
dispatch
"send confirmation"
!
requires change & redeployment
🔥 crazy stuff is going on here
!
DO NOT DO THIS
🔥
1. Publishers should be unaware of their subscribers
2. Subscribers should be unaware of publishers
Pub/Sub Principle
VIOLATION
This is how we unfortunately use Redux 🤦🏻♀️
An action type should represent an important business event ... Not a state mutation / command
Example:
Soccer Game App
Business Requirement
When a player scores a goal:
- increment the scoreboard
Command Action Example
// in scoreboard reducer.js
const INITIAL_STATE = {
home: 0,
away: 0
};
function scoreboardReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "INCREMENT_SCORE": {
const scoringSide = action.payload;
return { ...state, [scoringSide]: state[scoringSide] + 1};
}
default: return state;
}
}
Business Requirement
When a player scores a goal:
- increment the scoreboard
- crowd gets more excited
Command Action Example
// in scoreboardReducer.js
const INITIAL_STATE = {
home: 0,
away: 0
};
function scoreboardReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "INCREMENT_SCORE": {
const scoringSide = action.payload;
return { ...state, [scoringSide]: state[scoringSide] + 1};
}
default: return state;
}
}
//in crowdExcitmentReducer.js
const INITIAL_STATE = 0;
function crowdExcitementReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "INCREASE_CROWD_EXCITEMENT": return state + 1;
default: return state;
}
}
Command Action Consequences
// in GameComponent
class GameComponent extends React.Component {
scoreGoal() {
dispatch({ type: "INCREMENT_SCORE", scoringSide: "home"});
dispatch({ type: "INCREASE_CROWD_EXCITEMENT"});
// potentially more dispatches
}
render() {
//...
}
}
Disadvantage of Command Actions
- does not capture business semantics
- actions are coupled to reducers
- too many actions are firing
- unclear why state is changing
- leads to a lot of boilerplate
- does not scale
Eventful Action Example
// in scoreboardReducer.js
const INITIAL_STATE = {
home: 0,
away: 0
};
function scoreboardReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "GOAL_SCORED": {
const scoringSide = action.payload;
return { ...state, [scoringSide]: state[scoringSide] + 1};
}
default: return state;
}
}
//in crowdExcitmentReducer.js
const INITIAL_STATE = 0;
function crowdExcitementReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "GOAL_SCORED": return state + 1;
default: return state;
}
}
Eventful Action Example
// in GameComponent
class GameComponent extends React.Component {
scoreGoal() {
dispatch({ type: "GOAL_SCORED", scoringSide: "home"});
}
render() {
//...
}
}
Advantages of Eventful Actions
- clarity over why state is changing. i.e. the reducer is a spec file.
Eventful Action Example
// in scoreboardReducer.js
const INITIAL_STATE = {
home: 0,
away: 0
};
function scoreboardReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "GOAL_SCORED": {
const scoringSide = action.payload;
return { ...state, [scoringSide]: state[scoringSide] + 1};
}
default: return state;
}
}
//in crowdExcitmentReducer.js
const INITIAL_STATE = 0;
function crowdExcitementReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "GOAL_SCORED": return state + 1;
default: return state;
}
}
Advantages of Eventful Actions
- clarity over why state is changing. i.e. the reducer is a spec file.
- actions and reducers are not coupled
- less boilerplate
1 Action ≠ 1 Reducer
Command Dispatcher
// in GameComponent
class GameComponent extends React.Component {
scoreGoal() {
dispatch({ type: "INCREMENT_SCORE", scoringSide: "home"});
dispatch({ type: "INCREASE_CROWD_EXCITEMENT"});
// potentially more dispatches
}
render() {
//...
}
}
Eventful Dispatcher
// in GameComponent
class GameComponent extends React.Component {
scoreGoal() {
dispatch({ type: "GOAL_SCORED", scoringSide: "home"});
}
render() {
//...
}
}
Advantages of Eventful Actions
- clarity over why state is changing. i.e. the reducer is a spec file.
- actions and reducers are not coupled
- less boilerplate
- design using event-driven architecture, BDD & event storming techniques
WHEN user scores a goal, THEN increment score
Actor
Event (Action)
Side Effect
Code Location Matrix
Actor | Side Effect | Code Location |
---|---|---|
User | State | Reducer |
User | Not State (scheduling, http, storage, etc) | Custom Side Effect Handler |
Not the User (time, server, etc) | State | Custom Side Effect Handler & Reducer |
Not the User (time, server, etc) | Not State (scheduling, http, storage, etc) | Custom Side Effect Handler |
Tips on Writing Good Actions
- Think of the important business event that is worth reporting.
- DO not couple it to the UI implementation
- Ensure it can potentially be used by many reducers / side effect handlers.
In Summary
Q & A
Our Redux Anti Pattern
By Yazan Alaboudi
Our Redux Anti Pattern
In this talk, we will walk you through how Redux is an instance of a Publish/Subscribe (Pub/Sub) model. I’ll use that perspective to redefine what an "Action" should represent to enable scalability with ease. To benefit from this talk, you must have a fundamental understanding of Redux.
- 34,467