MVC is dead - even for AngularJS

MVC is dead, MVC will remain dead. It served us well over those years but it's time to explore and use better alternatives for state management in AngularJS.

Intro

This article is a follow up of the talk that i give for AngularJSDublin. I have to thank all the guys attending the meetup for the awesome feedback that i received.

Let's start by analyzing very high level the evolution of the FE architecture patterns.

FE Architecture

Before 2010 we can say that Jquery was the absolute king on the FE. Who doesn't remember those days i can tell you it was mainly spaghetti code

In 2010 frameworks like Backbone started to introduce MVC in their core, what they called MV* most of the time.

Backbone and other frameworks a like took this pattern from the BE, and it was great thanks to the big separations of concerns that MVC brought to the FE.

In 2012 AngularJS v1 was released, 4 years ago really that long. Also AngularJS followed the MV* approach introducing something even more powerful on top of it.

2 way binding

Any data-related changes affecting the model are immediately propagated to the matching view(s), and that any changes made in the view(s) (say, by the user) are immediately reflected in the underlying model. When app data changes, so does the UI, and conversely --Stackoverflow

Now all our views will update when the model change and vice versa. That was the real superpower of angular.

If we take 2 way binding and apply it over AngularJS components, if we update a property on the child it will be updated also on the parent and same thing apply for parent child.

This is a common use of two way binding for components communication.

.component('parent', {
  controller() {
  }
})

.component('child', {
  bindins: {
   twoWay: '='
  },
  controller() {
  }
});
<!-- Parent -->  
<child  
  two-way="$ctrl.twoWay">
</child>

<!-- Child -->  
<input ng-model="$ctrl.twoWay" type="text">  

I like to compare for this 2 way binding to alcohol:

Homer

It's the solution and cause to all the AngularJS problems.

Try to imagine when your application start to grow and you have multiple children connected to the same parent and all of them are changing your twoWay model.

What it looks time to me is:

Solitarie

There are some alternative ways and they mainly comes from the React world. In 2015 Redux was released and it is probably the most famous implementation of Flux. What actually Redux and Flux bring to the table?

One way dataflow

The model is the single source of truth. Changes in the UI trigger actions/messages that signal the intent to change the model.

One way dataflow is a deterministic approach while two-way binding can cause side effects and that's what we are trying to avoid.

To apply one way dataflow i recommend use a fantastic manual by Todd Motto: stateful-stateless-components.

If we want to convert the previous example using one way dataflow:

.component('parent', {
  controller() {
    this.onTwoWayChange({value}) => {
      this.twoWay = value;
    };
  }
})

.component('child', {
  bindins: {
   twoWay: '<',
   onChange: '&'
  },
  controller() {
    this.modelChange = (value) => {
      this.onChange({
        $event: { value }
      });
    };
  }
});
<!-- Parent -->  
<child  
  two-way="$ctrl.twoWay"
  on-change="$ctrl.onTwoWayChange($event)>
</child>

<!-- Child -->  
<input  
  ng-model="$ctrl.model"
  ng-change="$ctrl.modelChange($ctrl.model)
  type="text">

What we are gaining here is: consistency with angular 2 components communication and predictable changes of our model and we also finally decoupled the child components from the parent.

Those components are also called stateful and stateless components:

  • Stateful component

    Stores information about the app, has the ability to change it and also will render child components.

  • Stateless component

    It render it's inputs and send an output

Kill MVC

Uma Thurman

Now that we have introduced one way dataflow applied to components we can also kill MVC directly. The problem is not really about MVC, rather that FE apps have become more and more complex over the years and MVC has always coupled our controllers directly to our views with the consequences that our code cannot respond to rapid changes as it's usually the case.

Redux

Redux is a predictable state container, the purpose of Redux is to make state mutations predictable.

The three principles of Redux are:

  • Single source of truth

    The state of your app is stored inside a single store

  • State is read only

    The only way to change the state is to emit an action which is an object describing what happen.

  • Changes are made with pure functions

    To specify how the state is transformed by actions you write pure reducers.

A reducer is a pure function that take the previous state and an action and return the next state.

Redux diagram

If you want to learn more about Redux i strongly recommend to read the online documentation that you can find at http://redux.js.org/

ng-redux

On angular 1 applications you can use ng-redux a relative small library that will connect the store to the angularjs world.

Here's is an example on how to connect to create a store:

.config(function($ngReduxProvider) {
  $ngReduxProvider.createStoreWith(reducers, middlewares, enhancer, {});
})

createStoreWith take as first argument a single reducer composed of all other reducers. To check the additional arguments you can have a look at the documentation here https://github.com/angular-redux/ng-redux.

Let's take a look on how to connect a component to the store

export const ReduxComponent = {  
  template,
  controller($ngRedux, ReduxActions) {
    'ngInject';

    const mapStateToThis = (state) => {
      return {
        slice: state.slice
      };
    };

    this.unsubscribe = $ngRedux.connect(
      mapStateToThis,
      ReduxActions)((state) => {
        Object.assign(this, state);
      });
  }
};

When the component initialize we connect to the store using $ngRedux.connect where we pass a function to slice the state and the actions that we want to connect and will return a function with the state sliced.

The actions will be available directly to the controller of the connected component so you can just call the actions
to trigger them.

Remember always to unsubscribe from further store updates using the function returned from the connect, just adding on the eg:

controller($ngRedux, ReduxActions) {  
  ...
  this.$onDestroy = () => {
    this.unsubscribe(); 
  };
}

Async reducers

All ng-redux documentation covers only the case of reducers defined at the start of our application. For simple application this is enough to cover your needs but on large applications where you are probably using ocLazyLoad there will be scenarios where you don't want to load reducers upfront, but instead leveraging lazy loading of modules, the module itself should add a reducer dinamically to the store.

For dinamically load reducers just using redux you can follow this stackoverflow (http://stackoverflow.com/questions/32968016/how-to-dynamically-load-reducers-for-code-splitting-in-a-redux-application).

Now in AngularJs we have another layer on top of Redux that initialise the store for us.
A solution which i need to thank Krisztián Huterják for it a coworker of mine is the following:

.config(function($ngReduxProvider) {
  'ngInject';
  $ngReduxProvider.createStoreWith(state => state, middlewares, enhancer, {});
})

We initialize the store with the reducers that we have available at that time, in this case its just the identity function. This code should be included into your main module that is always loaded.

Then we decorate $ngRedux adding the ability to addReducers on the fly.

.config(($provide) => {
  $provide.decorator('$ngRedux', ($delegate) => {
    const reducers = {};

    const rootReducer = (state, action) => {
      const keys = Object.keys(reducers);
      if (keys.length === 0) {
        return Object.assign({}, state);
      }
      const nextState = Object.assign({}, state);

      keys.forEach(key => {
        nextState[key] = reducers[key](state[key], action);
      });
      return nextState;
    };

    $delegate.addReducers = (reducersObject) => {
      Object.assign(reducers, reducersObject);
    };

    $delegate.replaceReducer(rootReducer);

    return $delegate;
  });
})

And when we need to add a new reducer we then simply call addReducers.

.run(function($ngRedux) {
  $ngRedux.addReducers(asyncReducer);
})

Conclusion

MVC is dead, MVC remains dead, and we have killed him. How shall we model our apps, the murderers of all murderers!

What was the holiest and mightiest of all that the frameworks had, yet owned has bled to death under our typing.

Who will wipe this pattern off to use?

What approach is there for us to use ourselves?

What flow of atonement, what sacred pattern shall we to invent?

Is not the greatness of the deed too great for us?

Must we ourselves embrace the flow simply to appear worthy of it?

This is a paraphrase of God is dead from Thus spoke Zarathustra, instead of embracing nihilism you can embrace Redux.

Thank you.