marko-js / marko

A declarative, HTML-based language that makes building web apps fun
https://markojs.com/
MIT License
13.25k stars 643 forks source link

question - mobx integration #853

Closed cameronbraid closed 3 years ago

cameronbraid commented 6 years ago

http://markojs.com/docs/marko-vs-react/ mentions that markojs is compatible with MobX.

MobX provides a decorator/function for turning a react component into an observer which automatically re-renders the component when the dependent observables change.

How would something like that be done with markojs ?

patrick-steele-idem commented 6 years ago

Hey @cameronbraid, admittedly, we have not tried out MobX so I can't provide the exact steps on how to integrate MobX. Also, in my opinion the MobX docs don't do a great describing how to set up a general observer to a data store but I have not spent that much time in the docs. We would welcome contributions from Marko users to provide bindings/sample code for MobX+Marko.

What I can share with you is how simple it is to integrate with Redux:

import store from './store';

class {
    onMount () {
        store.subscribe(() => {
            // Force this UI component to rerender:
            this.forceUpdate();

            // The UI component will be rerendered using the new
            // state returned by `store.getState()`
            //
            // The following is another option to force an update:
            // this.input = store.getState();
        });
    }
}

<div>
    <counter(store.getState()) ... />
</div>

In the above code the ./store module is just a Redux store created using redux.createStore(...).

Would you be interested in contributing MobX examples @cameronbraid ? If so, that would be greatly appreciated.

cameronbraid commented 6 years ago

I have only discovered MobX today.

Marko re-renders when input or state changes, and from what I can gather MobX observable objects and arrays don't create new objects or arrays upon state changes - so there would need to be some way to decorate the render method (to make it an observer) like they do for react.

patrick-steele-idem commented 6 years ago

When forceUpdate() is called it will result in that particular UI component re-rendering regardless of whether or not state or input changed. However, once the data gets passed down to nested UI components the nested UI components won't be forced to re-render. For that reason, we strongly recommend immutable data to be passed around. Unless I am mistaken, MobX still promotes immutable data.

I'm curious, why not use Redux? Also, sometimes it is just simpler and sufficient to just use your top-level UI component as a centralized state store. I'm not saying there is never any use for libraries such as Redux and MobX, but they are not always needed.

cameronbraid commented 6 years ago

I'm fairly familiar with how marko works - as I use marko in https://www.drivenow.com.au for the search/details/book flow. It was migrated from an angular v1.0 app, using handlebars templates for the search view (angular was too slow). Is it now fully implemented in markojs - so thanks - MarkoJs is really good !

I was able to re-use the stores from the angular app even though they are mutable, as they used internal dirty tracking via versions for each level in the model So I could do this <custom-tag store=store v=store.v/> and have the custom-tag re-rendered when store.v changes. But I digress.

I have done some development using redux, but it required a lot of boilerplate code. When searching for ways to reduce redux boilerplate I came across MobX and though : that looks pretty interesting. So I'm considering using markojs + mobx for admin areas of the site where the data model is much more complex. So using something like mobx makes it pretty trivial as you just write normal javascript code and decorate it with observable/action etc.. but the missing piece is how to integrate it with marko.

The react observer https://github.com/mobxjs/mobx-react/blob/master/src/observer.js looks pretty tightly integrated with react's component model. And I think something similar would be needed in marko to fully automate the decoration of the tags to make them observers.

An other option is mbox-state-tree as it uses mbox behind the scenes. So it may be a more suitable candidate to be used in a similar way that redux is used in marko (subscribe for changes, extract snapshot upon change, then pass snapshot into tags via inputs)

cameronbraid commented 6 years ago

I tried using mbox-state-tree using snapshots but they are normalised so references contain the id rather than the referenced model. So that's not a suitable solution.

I found this article using mobx and virtual-dom without react https://medium.com/@botverse/enjoying-mobx-jsx-and-virtual-dom-621dcc2a2bd5 They use mobx.autorun to trigger the view to be re-rendered. This could work at the integration point where I mount the dom that marko rendered, however it doesn't address the issue of components identifying that their inputs have changed, since the inputs will have the same identity.

Is there a way to customise the way marko compares inputs ?

scott-cornwell commented 6 years ago

Hey, wanted to chime in here as I've been really liking MobX but wishing I could use it with Marko. I was able to get Marko working with MobX, with very few changes!

The catch is I did have to modify the Marko source by adding 3 lines of code in only one place. I am able to share state between components and it only renders what changed to the DOM. It seems promising but not sure if there are any performance implications as it's a bit of a hack. I will try and post a working example soon, but here's the gist of what I did:

modify marko/src/components/renderer.js near the bottom and after this existing line:

templateRenderFunc(input, out, componentDef, component, component.___rawState)

add the following 3 lines:

component.templateRenderFunc = templateRenderFunc
component.out = out
component.componentDef = componentDef

Then I create this small function:

import { autorun } from 'mobx'

const bindMobx = (component) => autorun(() => {
  component.templateRenderFunc(component.input, component.out, component.componentDef, component, component.___rawState)
  component.forceUpdate()
  component.update()
})
export { bindMobx }

We're almost done! Now instead of with mobx-react where you put @observer on the Component, we import bindMobx and use it in our Marko component like so:

class {
  onMount() {
    this.unbindMobx = bindMobx(this)
  }

  onDestroy() {
    this.unbindMobx()
  }
}

$ let store = input.store
<div>
  <span>Text: ${store.text}</span>
</div>

I plan to investigate more into this, but it would be great if @patrick-steele-idem or @mweststrate could weigh in on this. I have just barely dug into the internals of MobX or Marko, so anything like if there's a better way to get at the Marko template function from inside a component (without hacking at Marko), or if there's a better way to tell MobX when to update I'm all ears.

I think maybe a better way would be to somehow get all the variables used in Marko render function without actually calling the function, and then dynamically generate a function to autorun that just references those variables on the store.

I am currently using marko 4.4.19 and mobx 3.2.1.

cameronbraid commented 6 years ago

I had some spare time so I had a quick go at implementing a marko component (mobx-observer.marko) and transformer (mobx-observer-transformer.js) to let you make any marko component observable.

A simple example : note the mobx-observer attribute on the span tag in app.marko

model.js

const extendObservable = require('mobx').extendObservable;
module.exports = extendObservable({}, {
  count : 0,
})

app.marko

static {
  const model = require("./model.js")
}
class {
  increment() {
    model.count++
  }
}
<span mobx-observer>
   <button on-click("increment")>increment</button>
  ${model.count}
</span>

One thing I don't like is that mobx-observer.marko has to use a span tag at its root - without it marko errors "Uncaught TypeError: Cannot read property 'nextSibling' of null" which appears to be a bug in marko

Apart from that it appears to work quite well, and without any code changes to marko

scott-cornwell commented 6 years ago

Thanks for posting this Cameron, I'll have to do a performance comparison soon. I was able to make some tweaks to my version that improved performance, such as mocking the functions on the out object in my autorun. I think my version may hurt performance, but is still quite usable as it seems to perform about the same as regular React with setState. I can can update 1000-2000 components simultaneously with no noticeable lag. I guess I need to compare plain Marko without MobX as well.

I could see it being very useful if in addition to generating a template render function, more frameworks had the ability to create a minimal function that references all the variables used in the template render. That way any framework that can create observables just based on references could easily integrate with it.

My guess is that the main reason performance would be hurt is having to call forceUpdate and update, as just calling forceUpdate only updates the first component.

cameronbraid commented 6 years ago

FYI I based the core parts (reaction, reaction.track, extras.allowStateChanges) from the official mobx-react project. See https://github.com/mobxjs/mobx-react/blob/master/src/observer.js

There are a lot of corner cases catered for in the react version, so be warned that my demo is a demo - it is very minimal (i.e incomplete), and I have no automated tests.

In regards to forceUpdate.. you just need to use mobx-observable in enough places. For example in my demo if you update just the count it only re-renders the mobx-observable closest parent.

https://github.com/mobxjs/mobx-react has this information in their FAQ :

Should I use observer for each component?

You should use observer on every component that displays observable data. Even the small ones. observer allows components to render independently from their parent and in general this means that the more you use observer, the better the performance become. The overhead of observer itself is neglectable

scott-cornwell commented 6 years ago

Oh I'm talking about my marko w/ mobx implementation, so there is no observer to use. I have a giant list of the same component, so they all depend on the same counter and have the bind code in my first post above, but without forceUpdate() and update() called in the bind part it only ever updates the first component (they are all siblings). I've been using Mobx with React for a bit and haven't had any problems.

jesse1983 commented 5 years ago

Hi guys.

I created a package using Mobx compatible with MarkoJS.

Feel free for download it:

https://www.npmjs.com/package/mobx-event

richardaum commented 5 years ago

Any new about this? I really wanted to use MobX along with MarkoJS.

DylanPiercey commented 3 years ago

User-land implementations such as https://www.npmjs.com/package/mobx-event are the best path forward. There will not likely be built-in support for mobx in the future.