Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global State Management #27

Open
ThaNarie opened this issue Nov 19, 2017 · 3 comments
Open

Global State Management #27

ThaNarie opened this issue Nov 19, 2017 · 3 comments

Comments

@ThaNarie
Copy link
Member

ThaNarie commented Nov 19, 2017

This is a two-part issue, since this needs to be solved for two situations:

  1. Normal Muban projects that only have .hbs components
  2. SPA-like Muban sections where we have an app-in-site that uses the normal knockout components/templates, and works fully client-side

1. Normal Muban

In this case it really depends on how much global state we need.

  • Most state only lives on the current page, and will be lost when the page is reloaded

  • Some state might be more persistent (cookie/LocalStorage), question is if we interface directly with the DOM APIs, or if we are going to abstract that away in our own global state. Examples are:

    • A toolbar/panel that stays visible during page navigation, but is unrelated to backend rendering
    • A popup that has been closed and should not trigger for x amount of days

Even if state only exists on the current page, it might still cross component boundaries; updates to data/state in one component should also reflect in other components. When this is required, you probably want to have some global state management in place.

2. SPA Muban

In some cases, one page of the website is a complex tool that only lives on the client. The backend might provide an initial data set (embedded or via API), and all logic happens on the client. Such a tool can consist of multiple virtual pages, and can become quite large.

The best solution depends on how much data/communication is needed between components.

Options

Singleton Model

The simplest option would be to have a Modal class that exports itself as a single instance. It can be required in all components, where they can read and write data.

The class would consist of multiple observables, so components can subscribe to them to receive updates.

Model:

class Model {
  constructor() {
    this.foo = ko.observable('bar');
  }
}

const model = new Model();
export default model;

ViewModel:

import model from '../data/model';

class Foo {
  constructor() {
    model.foo.subscribe(value => {
      console.log('updated: ', value);
    }
  }
}

However, when dealing with large (external) datasets, updating and detecting changes in deeply nested objects can become quite cumbersome. Other libraries like mobX and Vue deal with this in a way that in userland you working with normal objects, but in the background they are able to detect changes to them. This feature make those options more friendly to users. Unfortunately, knockout observables don't have this option.

Pros:

  • Simple setup
  • Observables provide basic functionality

Cons:

  • When dealing with data, nested observable structures can become cumbersome to work with.

Redux

By choosing redux as the global state container, and creating an interface to map the redux state to observables in components, we can make it easier to work with data.

I created a proof-of-concept function called knockout-redux-connect, where the interface is based on the connect function of react-redux.

When having the following component:

<link rel="stylesheet" href="./todos.scss">
<script src="./Todos.ts"></script>

<ul data-bind="foreach: params.todos">
  <li data-bind="text: $data.text"></li>
</ul>

<form>
  <input type="text" data-bind="value: newTodo" />
  <button type="submit" data-bind="click: addTodo">Add</button>
</form>

And the following class:

class Todos {
  private params: any;
  public newTodo = ko.observable();

  constructor(params) {
    this.params = params;
  }

  addTodo = () => {
    this.params.addTodo(this.newTodo());
    this.newTodo('');
  };
}

You can connect to redux like this:

const mapStateToParams = state => ({
  todos: state.todo.todos,
});
const mapDispatchToParams = dispatch => ({
  addTodo: (...args) => dispatch(addTodo(...args)),
});

export default connect(mapStateToParams, mapDispatchToParams)(Todos);

The params object is already used by Knockout components to receive data from a parent component. The connect function sits in between, and merges the object returned from its functions to pass it to the component.

When doing this (the first time), it converts the returned state object to observables by using the ko.mapping plugin. When the redux state changes at a later time, it uses that function again to updated the stored observables.

Using this method the component can use/subscribe the received observable params the same way as if they were passed from the parent component.

Actions work the same way as in redux, they can called from the component. Optionally we could unwrap the passed payload so it doesn't contain any observables before it's dispatched.

The normal handlebars components could also be updated to support this util.

Pros:

  • Use existing known libraries (redux)
  • Full support of the complete redux ecosystem, including middlewares, redux devtools, etc

Cons:

  • Maybe too much boilerplate/complexity when dealing with smaller projects
  • ko.mapping is not really maintained (used a fork that received some updates), so could contain bugs with a low chance of being fixed by others

Custom Stores

The ko.mapping plugin could also be used to create a custom store, similar to it's done in mobx/vue.

Vuex uses actions and mutation to update data in the store. These could be converted to observables after execution. It also has a concept of getters, which are like knockout computes, or redux selectors.

MobX uses uses actions as a context where all updates to the observables in the store are batched.

Pros:

  • Less complex
  • Still keeps observables out of the stores

Cons:

  • Same concern regarding the mappings as above
  • Not sure yet...

Feedback is much appreciated!
@flut1 (you have knockout and redux experience)
Edgar (you have knockout, redux and mobX experience)
@larsvanbraam, @mmricco, @ReneDrie (you have knockout and Vue experience)

mediamonks-arjan pushed a commit that referenced this issue Nov 22, 2017
Add knockout-loader, that allows you to import styles and a ViewModel
script from a knockout component template, which registers itself as
a knockout component.

Add a `knockout-redux-connect` function that connects knockout with
redux, converting the returned redux state to observables, and updating
them when the store changes.

Added documentation for the above; how to set up a knockout and
redux 'block' in your project.

re #27
@skulptur
Copy link
Contributor

Are we still considering this? The singleton models are very simple to use, anything more advanced can be decided per project. And if that is a common need then it should be a lib IMO.

@ThaNarie
Copy link
Member Author

This is 2,5 years old, so need to re-check if and how we still need it, and if the solutions still make sense, or if there are new options.

I hope that you and others who did more with muban have some insights/experience on what's needed/missing in this area.

@pigeonfresh
Copy link
Contributor

I'm using the singleton model for most of my projects. In my cases most of the use-cases are fairly simple or simple enough to not have to use a different method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants