mithril.js icon indicating copy to clipboard operation
mithril.js copied to clipboard

m.render inside route onmatch causes infinite loop

Open BobbyGerace opened this issue 6 years ago • 5 comments

In the code below, the onmatch function is called over and over again in a loop, and the Component is never actually rendered. I'm new to Mithril so I may just be doing something wrong, but I didn't see anything in the documentation indicating that I shouldn't do this.

const Component = {
    view: function() {
        return m('div', 'hello world')
    }
};

m.route(document.body, '/index', {
    '/index': {
        onmatch: function() {
            console.log('onmatch was called');

            // Show loading while we load data
            m.render(document.body, m('div', 'loading...'));

            return new Promise(function(resolve, reject) {
                setTimeout(resolve, 1000);
            }).then(function() {
                return Component;
            });
        }
    }
})

The issue seems to be with the call to m.render. If you comment that out it works fine.

BobbyGerace avatar Jul 26 '19 14:07 BobbyGerace

@SleepyPierre if you choose a different mount point to render the loading div, it'll work. E.g.: <div id=test></div> And then: m.render(document.getElementById('test'), m('div', 'loading...'));

osban avatar Jul 26 '19 14:07 osban

@osban That does work, but what I really want is for the loading div to replace the content of the current route while the next route loads. And the only way I can see to do that is if the mount point of the loading div is the same as the mount point of the routes.

If I just change the mount point of the loading div, then the loading would appear next to the content, and it does not go away once the route's content has loaded.

Obviously this is a contrived example and there is only one route, but I think you can see what I'm getting at.

BobbyGerace avatar Jul 26 '19 15:07 BobbyGerace

@SleepyPierre I think in that case it's easier to deal with it in the components, something like this. But you can also do it in the Route Resolver, like in the docs example.

osban avatar Jul 26 '19 15:07 osban

That worked, thanks!

I'll still leave this issue open, as I still believe it's a bug. (This worked in an older version, so I'm sure more people will come across this as they upgrade)

At the very least, there should probably be an error message and/or a note in the change log

BobbyGerace avatar Jul 26 '19 15:07 BobbyGerace

@SleepyPierre Your component should handle the temporary loading, not the onmatch. Think of it in terms of states. Something like this should work better:

// Make sense of the loading, so you can actually do stuff with it.
// And yes, I've been considering making this a core component:
// https://github.com/MithrilJS/mithril.js/issues/2282
function Async({attrs: {load}}) {
	let state = "pending", result
	new Promise(resolve => resolve(load())).then(
		v => { state = "ready"; result = v },
		e => { state = "error"; result = e }
	)
	return {
		view: function ({attrs: {pending, ready, error}) {
			if (state === "pending") return pending()
			if (state === "ready") return ready(result)
			if (state === "error") return error(result)
		}
	}
}

const Component = {
    view: function() {
        return m('div', 'hello world')
    }
};

m.route(document.body, '/index', {
    '/index': {
		render: () => m(Async, {
			async load() {
				await new Promise(resolve => setTimeout(resolve, 1000))
				return Component
            },
			ready: Component => m(Component),
			error: e => m("div", ["Whoops! ", e.message]),
        }),
    }
})

The infinite loop sounds like a bug, though. It should be stepping on m.mount's toes behind the scenes, not generating infinite loops. (m.mount should really be throwing if attempting to mount to a DOM node with a non-empty vnode tree.)

dead-claudia avatar Jul 26 '19 22:07 dead-claudia