Hyperapp: Mobx support?

Created on 21 Feb 2017  路  15Comments  路  Source: jorgebucaran/hyperapp

So, we have redux/elm like state management. But, what about mobx?

Here is their user guide.
Here is a 10 minute interactive tutorial.

Mobx picture

I used to write with Redux, but Mobx changed everything for me. Besides having data react automatically, the most important thing for me was being able to use classes with real references (no normalizing of data required). Also, Typescript... is amazing (Mobx is written in it).

Discussion

Most helpful comment

@jbucaran That is literally the most convoluted example I have ever come across. No one writes their MobX applications like that. I can rewrite it if you would like.

Here is a full application by Formidable Labs:

All 15 comments

But, what about mobx?

Hi @thinkpadder1 馃憢

HyperApp's is influenced by the Elm Architecture, hence the suitable comparison to Redux. In HyperApp, the model is theoretically immutable, components are stateless, pure functions and actions are a mixture of reducers (old state + data -> new state) and effects (reactions in Mobx).

Mobx seems to favor object-oriented programming, using classes/prototypes and stateful components. Use of observables, decorators and TypeScript seems also popular.

At a glance, Mobx seems entirely different to HyperApp's model, so I have no idea how it could be integrated with Mobx. I'd be interested in hearing your ideas, though. How would the API look like if Mobx was built in?

Also, just wondering, how large is Mobx?

I looked at their examples, found a simple countdown timer example and made the same thing in HyperApp. Let's see how they compare:

Countdown Timer: HyperApp

<script src="https://unpkg.com/hyperapp"></script>
const { h, app } = hyperapp
/** @jsx h */

const pad = n => n < 10 ? "0" + n : n

const humanizeTime = t => { 
    const hours = t / 3600 >> 0
    const minutes = (t - hours * 3600) / 60 >> 0
    const seconds = (t - hours * 3600) - minutes * 60 >> 0
    return `${hours} : ${pad(minutes)} : ${pad(seconds)}`
}

const initialTime = 10

app({
    model: {
        time: initialTime,
        pause: true
    },  
    reducers: {
        drop: model => ({ time: model.time - 1 }),
        pause: model => ({ pause: !model.pause }),
        reset: model => ({ time: initialTime })
    },
    effects: {
        step: (model, actions) => {
            if (model.time === 0) {
                actions.reset()
                actions.pause()

            } else if (!model.pause) {
                actions.drop()
            }
        }
    },
    subscriptions: [
        (_, {step}) => setInterval(step, 1000)
    ],
    view: (model, actions) =>
        <main>
            <div>{humanizeTime(model.time)}</div>

            <button
                onclick={actions.pause}
            >{model.pause ? "Start" : "Pause"}</button>

            <button onclick={actions.reset}>Reset</button>
        </main>,
})

View online

Countdown Timer: React+Mobx

<script src="https://cdn.jsdelivr.net/g/[email protected](react.min.js+react-dom.min.js),[email protected],[email protected]"></script>
<script src="https://unpkg.com/[email protected]/lib/mobx.umd.min.js"></script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script src="https://unpkg.com/mobx-react-devtools"></script>
mobx.useStrict(true);

var countdownTimerFactory = function(durationMilliseconds, options) {
    var intervalID;

    var timer = {
        id: _.uniqueId("countdownTimer_"),
        originalMilliseconds: durationMilliseconds
    };

    var settings = _.assign(
        {
            interval: 10,
            runTime: 0,
            resetOnComplete: true
        },
        options
    );

    mobx.extendObservable(timer, {
        durationAsMilliseconds: timer.originalMilliseconds,
        isTimerRunning: false,
        get isComplete() {
            return timer.durationAsMilliseconds <= 0;
        },
        get display() {
            return _.padStart(timer.minutesRemaining, 2, 0) +
                " : " +
                _.padStart(timer.secondsRemaining, 2, 0);
        },
        get durationAsDate() {
            return new Date(timer.durationAsMilliseconds);
        },
        get millisecondsRemaining() {
            return _.round(timer.durationAsDate.getUTCMilliseconds(), 2);
        },
        get secondsRemaining() {
            return timer.durationAsDate.getUTCSeconds();
        },
        get minutesRemaining() {
            return timer.durationAsDate.getUTCMinutes();
        },
        get hoursRemaining() {
            return timer.durationAsDate.getUTCHours();
        },
        get percentageComplete() {
            return 100 -
                _.round(
                    timer.durationAsMilliseconds /
                        timer.originalMilliseconds *
                        100,
                    2
                );
        },
        startTimer: mobx.action("startTimer", function() {
            timer.isTimerRunning = true;
        }),
        stopTimer: mobx.action("stopTimer", function() {
            timer.isTimerRunning = false;
        }),
        reset: mobx.action("resetTimer", function() {
            timer.stopTimer();
            timer.durationAsMilliseconds = timer.originalMilliseconds;
        })
    });

    mobx.autorun("countDownTimer", function() {
        if (timer.isTimerRunning) {
            intervalID = window.setInterval(
                function() {
                    mobx.runInAction("timer tick", function() {
                        timer.durationAsMilliseconds -= settings.interval;
                        if (timer.isComplete) {
                            timer.isTimerRunning = false;
                        }
                    });
                },
                settings.interval
            );
        } else if (intervalID) {
            if (settings.resetOnComplete && timer.isComplete) {
                timer.reset();
            }
            window.clearInterval(intervalID);
        }
    });

    return timer;
};

var Main = mobxReact.observer(function(props) {
    var timer = props.timer;
    return React.DOM.div(
        null,
        React.createElement(mobxDevtools.default),
        React.createElement(timerWithBar, { timer: timer }),
        React.createElement(timerControl, { timer: timer }, timerControl)
    );
});

var timerControl = mobxReact.observer(
    React.createClass({
        render: function() {
            var timer = this.props.timer;
            var button;

            if (timer.isTimerRunning) {
                button = React.DOM.button(
                    { onClick: this.handlePause },
                    "pause"
                );
            } else {
                button = React.DOM.button(
                    { onClick: this.handleStart },
                    "start"
                );
            }

            return React.DOM.div(null, button);
        },
        handleStart: function(event) {
            event.preventDefault();
            this.props.timer.startTimer();
        },
        handlePause: function(event) {
            event.preventDefault();
            this.props.timer.stopTimer();
        }
    })
);

var timerWithBar = mobxReact.observer(function(props) {
    var timer = props.timer;
    return React.DOM.div(
        { className: "active-blind" },
        React.createElement(timerPercentageCompleteRenderer, { timer: timer }),
        React.createElement(timerRenderer, { timer: timer })
    );
});

var timerPercentageCompleteRenderer = mobxReact.observer(function(props) {
    return React.DOM.div(
        { className: "progress-bar-container" },
        React.DOM.div({
            className: "progress-bar",
            style: { width: props.timer.percentageComplete + "%" }
        })
    );
});

var timerRenderer = mobxReact.observer(function(props) {
    var timer = props.timer;
    return React.DOM.div(null, timer.display);
});

var timer = countdownTimerFactory(10000);
ReactDOM.render(
    React.createElement(Main, {
        timer: timer
    }),
    document.getElementById("mount")
);

View online

Mobx example taken from mobx-examples/60-countdown-timer.

Comments

  • Mobx has a great ecosystem and very cool devtools, it's flexible and it molds to traditional imperative programming practices; it does not force you to think only in terms of immutable state and reducers. Of course, if you think functional programming is superior, this doesn't matter at all.

  • Mobx itself can feel frameworky, it's also pretty large, react bindings + mobx is like 64kb minified, and 20kb gzipped. Add React, router, fetch and friends on top and you have 1/10 of a megabyte or more. Now you can start writing your app...

  • Mobx has a bit more stuff you need to learn to use it effectively. Some of these concepts are familiar like actions, some use different naming conventions like reactions, some are _new_ like observables, derivations, decorators, autorun, etc. I think this is a neutral point, since you'd still want to learn all these stuff because: _knowledge is power!_

  • HyperApp's is still in its infancy, the User Guide is still a work in progress and we need more people to try it out and make cool stuff with it

  • HyperApp applications are functional out of the box, that's a selling point, but also presents a learning curve to newcomers used to traditional imperative / object-oriented programming.

The timer example is much too simple. Mobx was originally made because the creator was working on a project with a lot of domain models (more than 500 for his application alone). Imagine trying to create that in Redux/Elm and with functional programming only.

Here is an example of a todo domain store (best practices).

@thinkpadder1 Sure. That was just one of the examples in Mobx examples repository.

It seems the link you've posted links to mostly documentation, so it's kind of hard to say anything or make any hard comparisons. I'd rather work on actual examples.

Do you have an example where Mobx really shines? I'd love to code it using HyperApp and see where that takes me.

And I agree with you, a countdown timer is much too simple, but that also begs the question, how come Mobx's implementation of it is so complex?

@thinkpadder1 Mobx is on the left.

  • HyperApp's example adds a reset button and includes no dependencies; Mobx's has no reset button and includes both lodash and moment.js as dependencies.

mobx

EDIT: Update image to include HyperApp's vanilla implementation.

And why you deleted my comment? It is perfectly readable and is seen that Mobx absolutely doesn't fit well in elm-like project. It's not just it doesnt fit, it totally cant be compared - size and overhead at least.

@jbucaran That is literally the most convoluted example I have ever come across. No one writes their MobX applications like that. I can rewrite it if you would like.

Here is a full application by Formidable Labs:

@thinkpadder1 The example was taken directly from mobx's org, from the mobx-examples repo.

@jbucaran All those examples are written in ES5, without JSX, no transpiling... He was just showing that you could do it without ES6+, decorators, Typescript, etc.

@thinkpadder1 Thanks for the links, unfortunately, that's not what I asked for. I'm still agnostic about Mobx's superiority over Redux or TEA (The Elm Architecture).

I think the correct way to debate here, is not with words, but code. I'll ask again, please provide an example where Mobx shines (no hurries, if you can find one just post it here when you do) so that I (or anyone else) can try to implement its HyperApp's version and learn HyperApp's architecture's choice shortcomings.

All those examples are written in ES5 and without JSX... He was just showing that you could do it without ES6+, decorators, typescript etc.

Sure! That'd certainly improve things quite a bit, I know. 30%? 馃. _Still_, I think it's clear, coming from them, the counter example was a rather poor choice to represent Mobx.

I can rewrite it if you would like.

That's one option, another is to find a better example that we can work with. I am pretty sure I'm missing something obvious about Mobx.

EDIT: Update image to include HyperApp's vanilla implementation.

@jbucaran How about this remote control app for Google Play Desktop? It has a lot of side effects and inputs.

@thinkpadder1 Thanks, that's better than docs 馃槄. I was kind of hoping for some more down to earth type of examples. Something like HyperApp's Examples.

Mobx's org still has other examples that I didn't have time to look at, so I'll start by rewriting some of those to HyperApp. Maybe I'll find something.

While writing the countdown timer, I noticed a few things which led to a few issues today:

The bottom line, all this tinkering is good, we can learn from Mobx too.

@jbucaran Sorry, all the smaller examples I have found were just too simple (not what you would use MobX for). 馃槩

This contacts list application which is much simpler than the remote control one.

@thinkpadder1 Great! The contacts list one looks simple (in scope), so I could probably rewrite it to HyperApp ~in a couple of days~ someday.

Thanks for bearing with me 馃檱.

More examples are coming! In the meantime we should close here since hyperapp will not be adding mobx support anytime soon / ever.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jorgebucaran picture jorgebucaran  路  3Comments

jorgebucaran picture jorgebucaran  路  4Comments

VictorWinberg picture VictorWinberg  路  3Comments

jbrodriguez picture jbrodriguez  路  4Comments

jscriptcoder picture jscriptcoder  路  4Comments