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,036