Make tracking easy with redux middleware

As any good product company, we want to track every action our users do, so we can improve our app based on real data. After a month or so developing our React Native app, the multiple tracking calls spread on actions and components all over the code were driving me crazy. If my product manager asked me about a tracking, it would take me a while to find the trigger, event name and params of that tracked action.


Since we use Redux as a state controller and most of the actions we want to track produce a state change, a redux middleware was the best choice to do what we want. From Redux documentation:

middleware is some code you can put between the framework receiving a request, and the framework generating a response

The benefits

  • Clean code, easy to maintain and understand
  • Tracking events easy to find
  • Modularization

Project structure

Our project is structured in modules, each of them having components, actions, reducer etc. So, each module also have a tracking file, easy to find as it's on the same position of any module tree. Outside the modules, we have a tracking middleware that combine all tracking files in one (using the same idea of combineReducers).

Tracking on project structure

Tracking file

Let's take as an example a search module and we want to track which terms users are searching. The tracking file would look like:

// modules/search/tracking/index.js
const searchTracking = (prevState, action, nextState) => {
  switch (action.type) {
    case 'SEARCH_REQUEST':
      MyTrackingService.trackEvent(“Search”, {query: action.payload.query});
      break;
}
};
export default searchTracking;

Tracking middleware

As I said early, outside modules we have a tracking file to be used as a redux middleware, that looks like this:

// tracking/middleware.js
import searchTracking from "modules/search/tracking";
import authTracking from "modules/authentication/tracking";
import { store } from "state/store";
const trackingMiddleware = ({ getState }) => next => action => {
  const prevState = getState();
  const result = next(action);
  const nextState = getState();
  searchTracking(prevState, action, nextState);
  authTracking(prevState, action, nextState);
  return result;
};
export default trackingMiddleware;

Finally, we just need to connect our tracking middleware to our store:

// state/store.js
import { createStore, applyMiddleware } from "redux";
import reducers from "state/reducers";
import trackingMiddleware from "tracking/middleware";
const store = createStore(reducers, applyMiddleware(trackingMiddleware));
export { store };

Extra points

  • Create a good pattern for action names, so when you want to look for a tracking that is triggered by a click on a purchase button, for example, you'll easily find the tracking when you look for PURCHASE_CLICK
  • Add a 'context' key on action payloads and always include it on redux actions, so the tracking service can know whether your click on the purchase button came from the search card or from the product details screen, for example.
  • Since you have access to the whole state, add to your tracking some useful info about the current state of the app, like how many filters were selected when user clicked on a product search card, if the user was logged in, A/B test info and so on.

That's it. Hope it helps. Good luck! o/