Skip to content
Alex Bainter edited this page May 6, 2017 · 12 revisions

Motivation

As applications grow larger, so too does the complexity. In a large Knockout application, having a single view model for the entire app can grow incredibly difficult to maintain. Separating the app into smaller pieces with their own view model helps, but inevitably some of the properties need to be shared between multiple view models. This can be difficult to manage, and leaves the developer with a choice: let the view models access and modify each other's properties directly, or assign a "parent" view model to coordinate state between them.

Sharing Access

function ViewModel1() {
     const self = this;
     self.someProperty = ko.observable('My Property');
     window.viewModel1 = this; // make this accessible to other view models
}

function ViewModel2() {
     const self = this;
     self.someComputed = ko.computed(() => {
          return viewModel1.someProperty() + '... or is it mine?'
     });
     self.updateTheProperty = () => {
          viewModel1.someProperty('No, MY property.');
     };
}

This is the worst approach. In this example, ViewModel1 and ViewModel2 are tightly coupled together. It's difficult to understand their interactions with one another unless you can view them both.

Parent View Model

function ViewModel1() {
     const self = this;
     self.someProperty = ko.observable('My Property');
}

function ViewModel2(someProperty) {
     const self = this;
     self.someComputed = ko.computed(() => {
          return someProperty() + '... or is it mine?';
     });
     self.updateTheProperty = () => {
          someProperty('No, MY property.');
     };
}

function ParentViewModel() {
     const self = this;
     self.viewModel1 = new ViewModel1();
     self.viewModel2 = new ViewModel2(self.viewModel1.someProperty);
}

This is better. Now ViewModel1 and ViewModel2 aren't coupled together. However, they are both coupled to the ParentViewModel, which is responsible for coordinating properties between the two of them. ParentViewModel has to pull the required properties off of ViewModel1 and pass them to ViewModel2. Is there a better way?

A Better Way

Since someProperty is needed by both view models, it can be considered part of the app's state. Rather than letting the app state live inside the view models, let's promote it to its own object that each view model has access to.

const state = {
     someProperty: ko.observable('Our property');
};

function ViewModel1() {
     const self = this;
     self.someProperty = state.someProperty // copy the property from state so it can be bound to the view
}

function ViewModel2() {
     const self = this;
     self.someComputed = ko.computed(() => {
          return state.someProperty() + '... or is it mine?';
     });
     self.updateTheProperty = () => {
           state.someProperty('Yep, we share the property!');
     };
}

Now ViewModel1 and ViewModel2 are completely independent from one another. someProperty is stored in a state object which they both have access to. When ViewModel2 updates state.someProperty, the changes will be reflected in ViewModel1, and vice versa. Rather than a parent selecting which properties to give to each view model, the view models can decide for themselves which state properties they need.

However, this approach isn't perfect yet. It only works because state is defined in the same file as both view models. We need a way to access the state from other files. If only there were a library to do this...

knockout-store

knockout-store offers methods for getting and setting the state of your application. This allows you to set your state in one file and get it in another.

setState sets the state object for your entire application.

import { setState } from 'knockout-store';

const state = {
     someProperty: ko.observable('Our property');
};

setState(state);

The object passed to setState will be stored in an observable. Again, state itself is a Knockout observable. Since the properties of state are themselves observable in this example, you probably only want to call setState once.

After setting the state, we can access it in another file through the getState() method.

import { getState } from 'knockout-store';

const stateObservable = getState(); // this returns an observable which holds the value of the state.
function ViewModel1() {
     const self = this;
     self.someProperty = stateObservable().someProperty;    
}

export default ViewModel1;
import { getState } from 'knockout-store';

const stateObservable = getState();
function ViewModel2() {
     const self = this;
     self.someComputed(() => {
          return stateObservable().someProperty() + '...or is it mine?';
     });
     self.updateTheProperty = () => {
          stateObservable().someProperty('Yep, we share the property!');
     });
}

export default ViewModel2;

What an improvement! Rather than coupling our view models to one another, each one is free to use the properties from state as it needs to. If a property in state is updated, each view model will react accordingly.

connect

knockout-store offers an additional method, connect, for connecting your view models to the state without having to explicitly call getState. It is heavily inspired by the method of the same name from react-redux.

First, let's modify our view models a bit. Instead of calling getState in the files where the view models are defined, lets redefine the view models to expect the state parameters they need through a params object passed when instantiating the view model.

function ViewModel1(params) {
     const self = this;
     self.someProperty = params.someProperty;
}
function ViewModel2(params) {
     const self = this;
     self.someComputed(() => {
          return params.someProperty() + '...or is it mine?';
     });
     self.updateTheProperty = () => {
          params.someProperty('Yep, we share the property!');
     });
}

Why params? Knockout supports custom components. View models used for components can be passed an object through the params binding in the view layer.

Okay, great, but how do we get the properties from our state object onto the params object? Since Knockout usually instantiates our view models for us, we can't pass the state object in ourselves when that happens. This is where the connect method comes in. connect will wrap our viewmodel such that properties from our state object are attached to params when the object is instantiated. The first argument passed to connect should be a function for mapping state properties to the params object. This function, the first argument of connect, will be given our state object, and any properties of the object returned by the function will be attached to the params object of our view model when it's instantiated. See mapStateToParams in the example below.

Finally, we pass our view model to the function returned by connect, which will give us our wrapped view model.

function ViewModel1(params) {
     const self = this;
     self.someProperty = params.someProperty; // from the app state, mapped by mapStateToParams
}

function mapStateToParams(state) { // 'state' here is the result of getState()()

     // the properties of the returned object will be attached to params
     // when the view model is instantiated
     return {
          someProperty: state.someProperty
     };
}

const ConnectedViewModel1 = connect(mapStateToParams)(ViewModel1);
export default ConnectedViewModel1;

Now if we do ko.applyBindings(ConnectedViewModel1), someProperty from our state object will automatically be passed to ViewModel1. Let's do the same for ViewModel2.

function ViewModel2(params) {
     const self = this;
     self.someComputed(() => {
          return params.someProperty() + '...or is it mine?';
     });
     self.updateTheProperty = () => {
          params.someProperty('Yep, we share the property!');
     });
}

function mapStateToParams({ someProperty }) { // using some ES6 destructuring sugar
     return { someProperty }; // ES6 shorthand sugar
}

export default connect(mapStateToParams)(ViewModel2)

Here we have two independent view models with access to the app's state, but only the portions they actually need. Since the state property someProperty is an observable, our view models can subscribe to it, use it in a computed, or make changes to it, and both view models will update accordingly even though they are completely unaware of each other. This reduces complexity tremendously, since you can now write individual, independent view models while keeping them in sync with the state of your application. In addition, we now have a clear definition of the app state defined separately from the view models themselves.

Hopefully now you see the power of this library. In combination with custom components, you can split the parts of your application into smaller, manageable pieces which are only responsible for reflecting and modifying the state of your application, rather than accessing other components directly. For more advanced options, consult the API section of the readme.

Clone this wiki locally