Mithril.js: Allow multiple instances of mithril to run on the same page simultaneously.

Created on 27 Sep 2018  ยท  22Comments  ยท  Source: MithrilJS/mithril.js

Expected Behavior

I should be able to create multiple mithril instances on the same page.

// commonjs 
const createMithril = require('mithril/createMithril')
const myMithril = createMithril()

// module import
import createMithril from 'mithril/createMithril'
const myMithril = createMithril()

// from script element
const Mithril = window.m.createMithril
const myMithril = createMithril()

Current Behavior

There is one mithril instance. Consequently, there is only one application possible per web page.

Possible Solution

  1. Make a factory function or constructor to create a new mithril (hypertext) object with a fresh copy of the render service, request service, mount, and route objects.
  2. Make the mithril object exported in index.js use the factory function.

Context

I want to use mithril for two separate components on the same web page without one influencing the other.

Most helpful comment

Let's examine the tools that are currently available.

Global autoredraw

Anything mounted โ€“ via m.mount or m.route โ€“ will be subject to autoredraw.

Autoredraw ultimately calls m.render under the hood, and does so under the following conditons:

  1. Event handlers & XHR requests resolve
  2. The m.redraw command is invoked

Additionally, these render calls are made on a throttled basis: all redraw requests are queued and de-duplicated to occur within the next animation frame (or at a target rate of a maximum 50 draws per second). This helps with synchronicity and avoiding unnecessary redraws in quick succession that would tax performance but not make any visible difference to the user.

Controlled draw

This is when the author invokes m.render manually. Simply put, nothing happens unless the author explicitly asks for it, and it happens immediately.

On top of the above, every authorable virtual DOM entity โ€“ component, element, or fragment โ€“ can during the draw loop decide to opt out of redraw computation if it has an onbeforeupdate method that returns false. This can help with very complex trees which can run a simple logic check to determine that they have no need to recompute.


So we already have a don't-make-me-think option at one end and a total control option at the other. Experienced users can easily assist others if any questions arise from the above (why is it recomputing so often / why is it not redrawing, given XYZ): for the former, we have long-established reasoning and familiar mechanisms to advise on; for the latter, it's entirely up to author discretion. But ultimately it comes down to a choice between two options based on predictable conventions.

Introducing a half-way house option introduces design questions for which there's no easy answer: do you really want separate XHR redraw queues? Are the different mountpoints really completely isolated in terms of the model data they depend on, and when those models may update? Is it desirable to have them split across separate throttled redraw loops which resolve out of sync with each other? Do we want redraw throttling at all?

If the answer to all is yes, then we've effectively described the autoredraw system; if the answer is always no, then you may as well use m.render directly. If it's somewhere in between, we're incapable of offering a solution we'd be happy to push out and support โ€“ the burden / benefit ratio is too high (because the benefit isn't clear, the burden is becomes intolerably high in having to explain and debunk whatever questions and usage problems consumers end up with).


If you really do have a use case for a mix-&-match of the various questions above, your best bet is to write your own wrapper which fits your particular requirements. For instance, @foxdonut has devised a pattern called Meiosis where draws occur in direct response to updated models. Alternatively, I just wrote a wrapper that opts out of auto-redraw but keeps the throttled draw system with explicit instance-specific redraw commands. Maybe one of these fits your circumstance better than what's available out of the box โ€“ maybe there are other variations to explore. But a priori breaking Mithril out of the singleton pattern isn't desirable.

All 22 comments

I agree, regardless of how performant Mithril is, having one component trigger a redraw on a completely unrelated component feels like a big smell.

Of course, we could just embed multiple iFrames in the webpage and put a separate instance in each!

For your use case, multiple Mithril instances is most likely overkill. Multiple m.mount roots are already permitted and supported, and that should likely work for your use case..

@RobertAKARobin that's a feature, not a bug! In providing global auto-redraw out of the box by default, Mithril prioritises UI persistence and simplicity of application design regardless of data modelling strategy. If you want to opt out of this you can do so with m.render instead of m.mount to explicitly dictate when a given view should recompute, and if you want to get extra complicated, you can specify onbeforeupdate hooks conditionally resolving to false for sub-views that you don't want to recompute on any given draw. These options exist for extreme cases - as the docs say, second-guessing the Mithril diff engine is difficult and fruitless work: we recommend avoiding it and sticking to m.mount.

Multiple m.mount roots are already permitted and supported

I did not realize that that was the case. That is nice and may in fact solve my current use case. However, still feel like you should be able to have multiple instances to completely decouple one component with another and not have to use the library with a severe handicap (i.e. only using m.render for DOM updates).

In providing global auto-redraw out of the box by default, Mithril prioritises UI persistence and simplicity of application design regardless of data modelling strategy.

Is there any compelling reason for the library not to offer the option of a localized auto-redraw system out of the box while still providing a global auto-redraw system as the default? From an API standpoint this requires perhaps one additional line of code.

Loading mithril from a script tag:

// setup script
window.myM = m.create()
myM.mount(rootElement, rootComponent)

// somwhere else
myM.redraw()

With browserify:

// in myM.js
module.exports = require('m').create()

// in setup.js
const m = require('myM')
myM.mount(rootElement, rootComponent)

// in somethingElse.js
const m = require('myM')
m.redraw()

The choice between enabling a global redraw system for ease of use out of the box and the option of a localized redraw system for the sake of decoupling components seems to me to be a false choice since the two are not mutually exclusive.

Let's examine the tools that are currently available.

Global autoredraw

Anything mounted โ€“ via m.mount or m.route โ€“ will be subject to autoredraw.

Autoredraw ultimately calls m.render under the hood, and does so under the following conditons:

  1. Event handlers & XHR requests resolve
  2. The m.redraw command is invoked

Additionally, these render calls are made on a throttled basis: all redraw requests are queued and de-duplicated to occur within the next animation frame (or at a target rate of a maximum 50 draws per second). This helps with synchronicity and avoiding unnecessary redraws in quick succession that would tax performance but not make any visible difference to the user.

Controlled draw

This is when the author invokes m.render manually. Simply put, nothing happens unless the author explicitly asks for it, and it happens immediately.

On top of the above, every authorable virtual DOM entity โ€“ component, element, or fragment โ€“ can during the draw loop decide to opt out of redraw computation if it has an onbeforeupdate method that returns false. This can help with very complex trees which can run a simple logic check to determine that they have no need to recompute.


So we already have a don't-make-me-think option at one end and a total control option at the other. Experienced users can easily assist others if any questions arise from the above (why is it recomputing so often / why is it not redrawing, given XYZ): for the former, we have long-established reasoning and familiar mechanisms to advise on; for the latter, it's entirely up to author discretion. But ultimately it comes down to a choice between two options based on predictable conventions.

Introducing a half-way house option introduces design questions for which there's no easy answer: do you really want separate XHR redraw queues? Are the different mountpoints really completely isolated in terms of the model data they depend on, and when those models may update? Is it desirable to have them split across separate throttled redraw loops which resolve out of sync with each other? Do we want redraw throttling at all?

If the answer to all is yes, then we've effectively described the autoredraw system; if the answer is always no, then you may as well use m.render directly. If it's somewhere in between, we're incapable of offering a solution we'd be happy to push out and support โ€“ the burden / benefit ratio is too high (because the benefit isn't clear, the burden is becomes intolerably high in having to explain and debunk whatever questions and usage problems consumers end up with).


If you really do have a use case for a mix-&-match of the various questions above, your best bet is to write your own wrapper which fits your particular requirements. For instance, @foxdonut has devised a pattern called Meiosis where draws occur in direct response to updated models. Alternatively, I just wrote a wrapper that opts out of auto-redraw but keeps the throttled draw system with explicit instance-specific redraw commands. Maybe one of these fits your circumstance better than what's available out of the box โ€“ maybe there are other variations to explore. But a priori breaking Mithril out of the singleton pattern isn't desirable.

A typical use case situation where development would requiring multiple instances of Mithril would be a website running on a SaaS platform. For example, Shopify. Shopify requires you develop stores in a static development environment where your collections and data is accessed using liquid tags. Shopify also provides merchants with Ajax API's that return JSON-encoded responses which means you can manipulate, control and work with some of your store data using JavaScript but generally speaking, most data must be accessed using liquid.

This becomes rapidly cumbersome and when a store starts to requires more complex components and actions, rendering and creating Vanilla JS scripts that are initialized inline and infused with liquid is not the direction anyone should be taking, though Shopify condones this type of thing ๐Ÿ˜ท

This is when a SPA framework like React or Mithril becomes a requirement and multiple instances a necessity.

Why necessity

In Shopify (for example) you're going to need to render and mount components at various areas of the DOM because some parts of the dom will require the liquid generated data from Shopify and other parts of the dom will require javascript components that's main purpose would be to work in unity with the Ajax API Shopify offers or in most cases, store specific components which assist in the overall functionality of the site like Currency exchange, Shipping calculation or Collection filtering.

There is no real way around avoiding multiple instances in a situation like this because your layout will be partially rendered using liquid and partially rendered using Mithril instances which are mounted based on specific points in the DOM.

Below is a rough example of how things would look, the elements with an id attribute would be Mithril components:

<body>
  <nav>
    <ul>
      {%- for link in linklists.main-menu.links -%}
        <li><a href="{{ link.url }}">{{ link.title }}</a></li>
      {%- endfor -%}
    </ul>
    <button type="button" data-dropdown="cart">
      Cart
    </button>
    <div id="cart">
      <!-- Mount Cart Component-->
    </div>
  </nav>
  <main>
    <div class="sidebar">
      <ul>
        {%- for link in linklists.sidebar-menu.links -%}
          <li><a href="{{ link.url }}">{{ link.title }}</a></li>
        {%- endfor -%}
      </ul>
    </div>
    <div class="filter">
      {%- for tag in current_tags -%}
        {{ tag }}
      {%- endfor -%}
    </div>
    <div class="sorting">
      <div id="sorting">
        <!-- Mount Sorting Component-->
      </div>
    </div>
    <div class="collection">
      {%- paginate collection.products by 50 -%}
        {%- for product in collection.products -%}
          {%- include 'collection.product' -%}
        {%- endfor -%}
      {%- endpaginate -%}
    </div>
    <div class="footer">
      <div id="newsletter">
        <!-- Mount Newsletter Component-->
      </div>
    </div>
  </main>
  <footer>
    <ul>
      {%- for link in linklists.footer.links -%}
        <li><a href="{{ link.url }}">{{ link.title }}</a></li>
      {%- endfor -%}
    </ul>
  </footer>
</body>

What you will notice is that at various points an instance would be required and getting around this would be a complete refactor where one would mount at one point, but again it is not possible without really altering the layouts design.

Hopefully this will give you a guys a real world use cases where this type of feature would be of use.

I don't see how this is related to multiple mithril instances (isolated mounts with their own "local" redraw)? Your setup should work fine using multiple mount points, and global redraw between them would probably not be an issue?

@panoply That doesn't imply the use or need for local redraw, just multiple mount points (which is already fully supported and has been since before v1 was first released).

It's not about the setup, multiple mounts work fine.

My point here is that even those these are isolated mounts, It's the global redraw (flems) that is triggered on anything mounted with m.mount. In the above use case all mounts are all essentially separate components and should not influence one another.

Multiple instances would allow for choosing what mount points use global redraw and what mount points should use local redraw.

@panoply For the short term, check out this userland helper I wrote. Does that help at all? (It's compatible with both m.render and m.mount.)

But what actual issues are you experiencing from the extra redraws?

@panoply Could you file a new issue detailing your problems? The original bug here is about creating entire new library instances, not about anything related to rendering.

Closing because the original feature request has better alternatives that already exist.

you'll need to take this with a grain of salt bc i think it was a couple years ago and my memory is hazy. but i floated something similar (multiple mount points + isolation, iirc).

my usecase was i was creating something similar to a like button, but instead of just being a button, it displayed a chart and used mutationobservers to attach/mount. i think i hacked mithril to make multiple mounts possible and it worked well enough.

the problem though was if the user/host site was also running mithril. there wasn't a way to isolate my embeddable script. because of this, mithril has limitations that others don't.

it might be possible to get around that with iframes 2.0 web components, but i'm not sure?

i'd be interested to know if anyone has any strategies around shipping an embeddable mithril app.

Oops...hit the wrong button when I said I was closing. My bad.

@cmawhorter Please file a new issue about your concern.

my concern was identical to OP so opening a new issue seems unnecessary. multiple mounts in mithril finally shipped, so i suspect this will too eventually.

@cmawhorter Apart from global interference (something a browser-specific m.noConflict() could fix), I don't believe there should be anything in the way of multiple cooperating Mithril library instances. Our vnodes are themselves also cross-realm and mostly JSON-compatible. If that's all you need, please feel free to file a new issue.

the problem though was if the user/host site was also running mithril. there wasn't a way to isolate my embeddable script. because of this, mithril has limitations that others don't.

In my opinion, this is a significant reason to implement this feature. And sure, there might be some backwards way to get around this problem, but it will not be as simple or straightforward as simply allowing a new mithril instance to be created.

@cmawhorter Would an m.noConflict() be sufficient for your issue?

Also, it's worth noting that if you use it as a module, it won't pollute the global scope, and Mithril does play friendly to other library instances.

a lot of the discussion in this issue assumes you control the web page your mithril code is running on, or that your code is running in one browser window, or in a browser window at all.

personally, i'd prefer nothing to noconflict. i'm +1 on a factory that takes window as an argument.

even mithril-node-render has to hack around this and makes this the first step to using it require('mithril/test-utils/browserMock')(global) -- but that falls down in a lot of situations (as of last time i tried).

also -- unit tests with jsdom would be simple.

andddd, because it just feels right.

@cmawhorter File a new issue with that idea. I'm open to exposing a factory that accepts a window parameter - we already do that extensively internally for easier testing in Node, so it'd just be us doing that a bit less ad-hoc and exposing it formally.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andraaspar picture andraaspar  ยท  4Comments

barneycarroll picture barneycarroll  ยท  3Comments

marciomunhoz picture marciomunhoz  ยท  4Comments

tivac picture tivac  ยท  3Comments

isiahmeadows picture isiahmeadows  ยท  4Comments