Migrating to Warmer Times Ahead

Caviar’s consumer web frontend architecture has changed a lot over the years. It was built as one large Rails application with Slim used as…

Caviar’s consumer web frontend architecture has changed a lot over the years. It was built as one large Rails application with Slim used as the templating engine, and jQuery and CoffeeScript added to handle client-side logic. They were the tools most familiar to the people who used them at that time.

As modern web frameworks and libraries progress incredibly fast, so has Caviar’s codebase and the engineers’ skillsets. We’ve moved off Sprockets to bundle our web assets to the amazing Webpack and Yarn, and we’ve moved off using CoffeeScript (thanks to the life-saving decaffeinate tool written by a fellow Square!) into modern ES6+ and Babel — all while maintaining functionality, which by no means is an easy task for a large application with a small engineering team.

Most notably was following the trend of moving more logic to the client frontend to take advantage of increasing browser processing power. Back in early 2015, we introduced React into the codebase — at that point still a relatively new tech; nowadays, the popular open-source UI library and one of the core facets of modern day web development.

One of the best and core parts of React is its flexibility. As the docs say right on its homepage:

Learn Once, Write Anywhere We don’t make assumptions about the rest of your technology stack, so you can develop new features in React without rewriting existing code.

This was awesome for us — we could incrementally write the new stuff in React, take advantage of a highly active community, and integrate our new and shiny components within our Slim templates with the handy package react-rails. The choices we made at that time couldn’t go wrong, right?

The problem

The thing that hasn’t changed for the past couple of years was our use of Fluxxor. It’s a neat little library implementation of the Flux application architecture used to handle the flow of data through the rest of the application. To quote the official Flux docs on how the pattern works:

Flux eschews MVC in favor of a unidirectional data flow. When a user interacts with a React view, the view propagates an action through a central dispatcher, to the various stores that hold the application’s data and business logic, which updates all of the views that are affected. This works especially well with React’s declarative programming style, which allows the store to send updates without specifying how to transition views between states.

Sourced from Fluxxor’s documentation page hereSourced from Fluxxor’s documentation page here

It worked great for the time, but as our application became more and more massive, we had more and more state in the client that became hard to track and hard to keep right — mutations in state make it super hard to debug when something goes wrong, and dealing with asynchronous calls adds a whole other level of complexity. Fluxxor in particular was not ideal for our situation as time passed:

  • Mutations in our Fluxxor stores (plural) made it incredibly hard to keep track of when things changed.

  • Fluxxor required the use of mixins, a pattern considered harmful by one of the React core team developers. This meant that we still relied on React v15 and React.createClass() , an API that has since been deprecated in React v16.

  • The Fluxxor.js library hasn’t been updated since Sept 2015.

We decided as a team to move on to another popular option to manage data flow, the tried-and-true Redux. I won’t dive into why one might choose to use Redux over Flux, but an excellent argument can be found here. Redux has a number of obvious benefits over Fluxxor, including:

  • One store, and that’s it. This makes state much easier to keep track of rather than multiple different stores tied to different flux instances.

  • State is immutable — this makes it obvious for us when our state has changed, since a new state object must be returned each time anything has changed at all.

  • Redux is actively maintained, the documentation is excellent, and there are bountiful articles online about everything React and Redux => the Redux ecosystem is plain amazing. Stuck on a problem? There’s definitely a Stack Overflow post or a Github issue about that same situation.

The Redux Architecture: SourceThe Redux Architecture: Source

How data flows in a React and Redux Application: SourceHow data flows in a React and Redux Application: Source

Migration Process

Now the only work left to do after choosing to use Redux was to actually use the library.

Base gif from https://media.giphy.com/media/lQRwl2XKnHJWE/giphy.gifBase gif from https://media.giphy.com/media/lQRwl2XKnHJWE/giphy.gif

Migrating the whole Caviar frontend in one go was simply not feasible. The codebase is too big — it would take too long, and there would be too many places for a mistake to occur.

To err on the side of caution, we needed to find independent portions of the codebase that we could rewrite without affecting other pieces. Caviar for Teams is part of Caviar, its codebase is relatively small compared to the whole Caviar codebase, and the experience is completely independent of the regular, single-diner consumer flow.

To begin our migration process, we needed to understand exactly where Flux was used, and how it was tied to our application. Notably, we knew we had to change and refactor the following portions of the codebase:

  • The Flux stores: the objects that maintain the data for the application domain.

  • The Fluxxor.Flux classes: the objects that allow us to access the stores and actions, and manages the dispatcher.

  • The components and containers that received state from the Flux stores.

For us, it made sense to experiment by starting with a rough and exact conversion from Flux to Redux — as it turns out, the process to map from Flux to Redux was relatively straightforward. Our Flux actions could map into an equivalent Redux action, our Flux stores could be mapped to Redux reducers, and finally, our React components could be rewritten to use Redux instead.

We were ready to begin the migration.

Photo by William Stitt on UnsplashPhoto by William Stitt on Unsplash

Actions

Action(s) in both Redux and Flux simply refer to objects that

…represents an intention to change the state.

Actions are the only way to get data into the store.

…represent the intent to perform some manipulation of the data in a Flux application…

The only way to update stores is to send them actions.

An example is given below of how synchronous and asynchronous actions are created in our Flux class.

import Fluxxor from 'fluxxor';

import { SYNC_ACTION, PENDING_ACTION, SUCCESS_ACTION, FAILURE_ACTION } from 'constants/action-types';
import HTTPService from 'services/http';
import SampleStore from 'stores/sample_store';

class SampleFlux {
  constructor() {
    this.stores = {
      SampleStore: new SampleStore(),
    };
    
    this.actions = {
      // called in React components
      syncAction(data) {
        this.dispatch(SYNC_ACTION, { data });
      },
      
      // called in React components
      asyncAction(url) {
        this.dispatch(PENDING_ACTION);
 
        HTTPService.fetch(url)
          .then(response => response.json())
          .then(json => this.dispatch(SUCCESS_ACTION, { json })
          .catch(error => this.dispatch(FAILURE_ACTION, { error: error.message });
       },
      };
    
    this.flux = new Fluxxor.flux(this.stores, this.actions);
  }
}
 
export default SampleFlux;

Our React components contain a reference to a SampleFlux class, and call flux.actions.syncAction(...) and flux.actions.asyncAction(...)to trigger calls to update the Flux store (example below in the Components section).

Mapping synchronous Flux actions to Redux actions is pretty straightforward; whereas the function syncAction in Flux called this.dispatch with the type of action and the payload data for the store, syncAction in Redux simply returns an object that contains the type and and payload.

To map asynchronous actions to our Redux actions, we used a helper library called redux-thunk to help map our asynchronous action creators into a version that can be called just like synchronous action creators. We can also reuse our existing service used to make HTTP calls.

import { SYNC_ACTION, PENDING_ACTION, SUCCESS_ACTION, FAILURE_ACTION } from 'constants/action-types';
import HTTPService from 'services/http';

export function syncAction(data) {
  return {
    type: SYNC_ACTION,
    data,
  };
}

export function asyncAction(url) {
  return (dispatch) => {
    dispatch(pendingAction());
    
    return HTTPService.fetch(url)
      .then(response => response.json)
      .then(json => dispatch(successAction(json)))
      .catch(error => dispatch(failureAction(error.message)));
  };
}

function pendingAction() {
  return {
    type: PENDING_ACTION,
  };
}

function successAction(data) {
  return {
    type: SUCCESS_ACTION,
    data
  };
}

function failureAction(error) {
  return {
    type: FAILURE_ACTION,
    error,
  };
}

These actions are bound to the dispatch function provided by Redux’s [bindActionCreators](https://redux.js.org/api-reference/bindactioncreators#somecomponent.js)() function and passed as props to the components via React-Redux’s [connect()](https://github.com/reduxjs/react-redux/blob/master/docs/api/connect.md#mapstatetoprops-state-ownprops--object). By doing so, the actions can be dispatched from the React components via simply calling this.props.syncAction(…) and this.props.asyncAction(…) to trigger an update in the Redux store (example below in the Components section).

Reducers

Reducers are pure functions from the functional programming world.

A reducer (also called a reducing function) is a function that accepts an accumulation and a value and returns a new accumulation. They are used to reduce a collection of values down to a single value. In Redux, the accumulated value is the state object, and the values being accumulated are actions. Reducers calculate a new state given the previous state and an action…

Reducers are the most important concept in Redux.

Converting from mutating the data directly in our Flux stores to Redux reducers and maintaining immutability was definitely much more complicated than converting the actions. However, lots of the logic in our Flux stores could be reused in our reducers, and thinking about the state update in terms of pure functions immediately made it easier to reason around — we knew exactly what went in and out of the reducers because they’re free of side-effects!

In our Flux stores, we initialize our state and bind each action type that we listen for to an associated handler function. In our handler functions, we mutate the state, and emit a change event that updates our components with new state.

import Fluxxor from 'fluxxor';

import { SYNC_ACTION, PENDING_ACTION, SUCCESS_ACTION, FAILURE_ACTION } from 'constants/action-types';

export const SampleStore = Fluxxor.createStore({
  initialize() {
    this.someState = {};
    this.isLoading = false;
    this.error = '';
    
    this.bindActions(
      SYNC_ACTION, this.syncActionHandler,
      PENDING_ACTION, this.pendingActionHandler,
      SUCCESS_ACTION, this.successActionHandler,
      FAILURE_ACTION, this.failureActionHandler,
    );
  },
  
  getState() {
    return {
      someState: this.someState,
      error: this.error,
    };
  },
  
  syncActionHandler(data) {
    // do something here to the state
    this.someState = data;
    this.emit('change');
  },
  
  pendingActionHandler() {
    this.isLoading = true;
    this.emit('change');
  },
  
  successActionHandler(data) {
    // do something here to the state
    this.someState = data;
    this.isLoading = false;
    this.emit('change');
  },
  
  failureActionHandler(error) {
    // do something here to the state
    this.error = error;
    this.isLoading = false;
    this.emit('change');
  },
});

Now instead in Redux, when the reducer listens for and receives an action with some action type, it will perform a switch statement based on that action type, and perform some logic in the right case statement. Depending on whether or not the state changed, the reducer either returns a newly created copy of the state with the updated values, or the existing state to indicate no change.

Depending on how complicated the store was, we could break down our mapped reducer via reducer composition to maintain readability and clarity of logic, or completely separate out the state into two or more reducers, and use Redux’s combineReducers to combine our smaller reducers into a single larger reducer. This is how we approached changing our Flux stores to reducers.

import { handleActions } from 'redux-actions';

import { SYNC_ACTION, PENDING_ACTION, SUCCESS_ACTION, FAILURE_ACTION } from 'constants/action-types';

const initState = {
  someState: {},
  error: '',
  isLoading: false,
};

export default handleActions({
  [SYNC_ACTION]: (state, payload) => {
    const { data } = payload;
    
    // do something here to the state
    return {
      ...state,
      someState: data,
    };
  },
  
  [PENDING_ACTION]: (state) => ({
    ...state,
    isLoading: true,
  }),
  
  [SUCCESS_ACTION]: (state, payload) => {
    const { data } = payload;
    
    // do something here to the state
    return {
      ...state,
      isLoading: false,
      someState: data,
    };
  },
  
  [FAILURE_ACTION]: (state, payload) => {
    const { error } = payload;
    
    // do something here to the state
    return {
      ...state,
      error,
      isLoading: false,
    };
  },
}, initState);

Notable differences between the Flux store and the Redux reducer include:

  • Taking into consideration how to treat the state as immutable under the Redux architecture compared to directly mutating the state under the Flux architecture. This means always returning a new state if the state has been updated via Object.assign() or the spread operator .

  • The boilerplate of explicitly emitting a change event in Flux is now gone this.emit('change')! Redux handles publishing for us, so the code is shorter and less repetitive.

At this point, we also added selectors to return specific parts of our state and perform calculations on the state that could be memoized in order to boost performance.

Components

Last but not least, we had to refactor our containers and components to receive state from Redux instead of Flux. Whereas before our components would receive state directly from the Flux stores via mixins and use this.state everywhere, our components now would receive the Redux state via props passed by Redux’s connect() and use this.props instead.

Hence the following component tied to Flux —

import React from 'react';
import PropTypes from 'prop-types';
import Fluxxor, { StoreWatchMixin } from 'fluxxor';

import SampleFlux from 'flux/sample_flux';

const FluxMixin = Fluxxor.FluxMixin(React);

const SampleFluxComponent = React.createClass({
  mixins: [FluxMixin, StoreWatchMixin('SampleStore')],

  flux() {
    if (!this.getFlux()) {
      this.context.flux = new SampleFlux().flux;
    }
 
    return this.getFlux();
  },

  getStateFromFlux() {
    return this.flux().store('SampleStore').getState();
  },
  
  handleClick() {
    this.flux().actions.syncAction('some sample data');
  },
  
  handleButtonClick() {
    this.flux().actions.asyncAction('some sample url');
  },
  
  render() {
    const { someState } = this.state;
    const { handleClick, handleButtonClick } = this;
    
    return (
      <div>
        someState && <p onClick={handleClick}>This is a sample component.</p>
        <button onClick={handleButtonClick}>This is a button.</button>
      </div>
    );
  }
});

export default SampleFluxComponent;

becomes the following component connected to Redux —

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux';

import * as actions from 'actions/sample_action_creators';
import * as SampleSelectors from 'selectors/sample_selectors';

class SampleReduxComponent extends Component {
  static propTypes: {
    someState: PropTypes.shape({}).isRequired,
    syncAction: PropTypes.func.isRequired,
    asyncAction: PropTypes.func.isRequired,
  }
    
  constructor(props) {
    super(props);
    
    this.state = {
      ...
    }
  }
    
  handleClick = () => {
    this.props.syncAction('some sample data');
  }
  
  handleButtonClick = () => {
    this.props.asyncAction('some sample url');
  }
  
  render() {
    const { handleClick, handleButtonClick } = this;
    const { someState } = this.props;
    
    return (
      <div>
        someState && <p onClick={handleClick}>This is a sample component.</p>
        <button onClick={handleButtonClick}>This is a button.</button>
      </div>
    );
  }
}
    
const mapStateToProps = (state) => ({
  someState: SampleSelectors.getSomeState(state),
});
    
const mapDispatchToProps = (dispatch) => bindActionCreators({
  syncAction: actions.syncAction,
  asyncAction: actions.asyncAction,
}, dispatch);
      
export default connect(mapStateToProps, mapDispatchToProps)(SampleReduxComponent);

And we’re done! The first pass of mapping Flux to Redux ends after we’ve updated our components, and they’ve successfully rendered and passed our tests (you should have plenty of tests!).

Photo by Massimo Sartirana on UnsplashPhoto by Massimo Sartirana on Unsplash

Takeaways

Immutability is so hard to enforce! The deeper the state object becomes, the more copying needs to be done to ensure references to the data that needs to change are updated. I ran into many quirks and bugs, where my selectors weren’t updating and the components were not re-rendering, because the state was mutated accidentally and the reducers were not returning a new state object.

I introduced Immutable.js to the reducers and selectors that I wrote. It guaranteed the immutability that I desired (woohoo!), but I noticed some issues:

  • Immutable’s API isn’t very friendly with native JavaScript. Whereas one can access properties on an object with dot notation for a regular JS object, Immutable requires use of special getter (.get(), .getIn()) and setter (.set(), .setIn(), .merge(), .update()) functions for their Immutable.Map objects. Whereas with JS objects one can do something such as the following at the end of a case statement in the reducer,
[ACTION_TYPE]: (state) => {
  return {
    ...state,
    someNestedObject: {
      someKey: true,
    },
  };
}

in Immutable, one would have to do something like:

[ACTION_TYPE]: (state) => {
  return state.setIn(['someNestedObject, 'someKey'], true);
}

which can get tricky. Though I’ve used Immutable in the past and am now comfortable with it, not all my teammates have, and hence there’s a steep learning curve that must be accounted for.

  • Immutable introduces massive amounts of scope creep. Not only does it affect pretty much everything in your reducers and selectors, to achieve the performance benefits of the library, you should also pass the immutable data structures into your components. This is a massive undertaking since our components expect regular JS objects and arrays, not Immutable Maps and Lists (and the API for get/set is completely different). This would take a long time to refactor all our components, and quite frankly we didn’t have enough resources to do this.

To battle this, we thought about using .toJS() at the end of our selectors so that we’d pass regular JS objects and arrays to our connected components, and this way we wouldn’t have to modify the logic in our existing components. However, any use of .toJS() is a performance hit to your application, and removes a lot of the performance benefits that Immutable provides in the first place.

Though introducing Immutable.js throughout our whole application would have removed some of the frustrations regarding mutability of data, and brought performance benefits to our components (shallow equality checks are faster than deep equality checks!), we ultimately decided to stash Immutable for now, and revisit this library in the future in another pass. A great write up and collection of links regarding this very exact topic (Immutable.js or not) can be found here.

Future Steps

Now that our application is working on Redux, it’s time to start thinking about cleaning up our frontend architecture and design. We need to figure out what state to store in Redux (like global application state) and what state to move to individual components (localized/UI state). This would mean a redesign of our state object and hence we decided to leave this to after the migration. We also need to start moving lots of computations out of our React components and memorize them in our selectors for further performance gains.

In the future, we’d definitely like to consider adding Immutable.js to handle the guarantee of an immutable Redux state. Other things also include persistence (redux-persist), routing (react-router), and upgrading to React 16.3 to take advantage of the new stuff and performance boosts (like fibers and new life-cycle methods and async rendering that I’m so so excited to use)!

Summary

As of this blog post, the Caviar for Teams corporate experience has completely moved off Flux in favor of Redux, and is now under a feature flag. In the near future, the rest of trycaviar.com will slowly migrate off Flux to Redux, and hopefully allow us to create and maintain the delightful experiences that diners have come to expect and trust of us.

Thanks to Nelson Crespo, Xiangjin Zou, Keith Chu, Madeline Ong, Tingshen Chen, and the rest of the Caviar team.

Table Of Contents
View More Articles ›