Skip to content

Conversation

@lydell
Copy link

@lydell lydell commented Nov 12, 2025

This takes all the learnings from elm-watch and implements hot reloading directly in Elm. I’m really happy with how it turned out, it’s pretty simple and clean. “Implements hot reloading” means that there’s now a Elm.hot.reload() JavaScript function that tools can use. It’s up to the tool to do file watching and stuff like that.

This also makes it possible to shut down an app, which is useful when embedding an Elm app in a web component or React component.

Those two are batched together since they touch on the same parts.

See the new javascript-interface.md file for all the details.

Companion PR:s:

lydell added 7 commits August 24, 2025 13:02
`Elm.hot.reload(newScope)` updates the `Elm` namespace and hot reloads
all running Elm app instances.

The compiler should first check that a hot reload is safe: If anything
has changed in the "deep canonicalized" type of `main`, it is unsafe.
Then the page should be reloaded instead.

It needs support in elm/browser to work.
(And in elm/virtual-dom for `main : Html msg` programs.)
These prevented apps from being garbage collected.
Ports are the only effect managers that can change. We need to not only
check if the port already exists in the previous code, but also make
sure that the correct data can flow through it, and that it goes in the
right direction.
@github-actions
Copy link

Thanks for suggesting these code changes. To set expectations:

  • Pull requests are reviewed in batches, so it can take some time to get a response.
  • Smaller pull requests are easier to review. To fix nine typos, nine specific issues will always go faster than one big one. Learn why here.
  • Reviewers may not know as much as you about certain situations, so add links to supporting evidence for important claims, especially regarding standards for CSS, HTTP, URI, etc.

Finally, please be patient with the core team. They are trying their best with limited resources.

Comment on lines +18 to +41
var _Platform_worker = F3(function(impl, flagDecoder, debugMetadata)
{
return _Platform_initialize(
flagDecoder,
args,
impl.__$init,
impl.__$update,
impl.__$subscriptions,
function() { return function() {} }
);
var init = function(args)
{
return _Platform_initialize(
flagDecoder,
args,
null,
null,
null,
function() { return function() {} },
impl
);
};

/**__DEBUG/
init.hotReloadData = {
__$impl: impl,
__$platform_effectManagers: _Platform_effectManagers,
__$scheduler_enqueue: __Scheduler_enqueue
};
//*/

return init;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All functions that wrap _Platform_initialize (also in elm/browser) have been changed this way:

  • Decrease from F4 to F3 and move the last argument to an explicit function (called init) inside. That’s equivalent – just another way to express the curriedness.
  • Attach .hotReloadData on that inner function in __DEBUG mode.

The inner function is called init because that’s the init function that ends up at Elm.Main.init for example. So for each Elm entrypoint, there’s .init() to initialize an app as usual, and .init.hotReloadData for use when hot reloading already initialized apps of that entrypoint.

Code that I’ll comment on later removes .hotReloadData, so there’s no risk that people start depending on it.

__Result_isOk(result) || __Debug_crash(2 /**__DEBUG/, __Json_errorToString(result.a) /**/);
var managers = {};
var initPair = init(result.a);
var initPair = impl.__$init(result.a);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’ll see this a lot in the diff: Instead of popping off stuff from from impl (like var init = imp.__$init; init()), we pass around the whole impl object and read from it all the time (impl.__$init()).

This allows us to simply mutate the impl object later (impl.__$init = newInit) to swap in new versions of the functions. That’s the core of how the hot reloading works!

Comment on lines +157 to +158
/**__DEBUG/
app.hotReload = function(hotReloadData)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like with .hotReloadData that I mentioned earlier, code that I’ll comment on later removes this .hotReload method on apps, so there’s no risk that people will depend on it.

In other words, in non-production mode we “smuggle” some more data/functions out to _Platform_export so it can create Elm.hot.reload – that’s the only exposed thing.

if (manager.__portSetup)
{
ports = ports || {};
ports[key] = manager.__portSetup(key, sendToApp);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for these changes is preventing a memory leak, that causes stopped apps never to be garbage collected. See the individual commits of this PR for the full story.

Comment on lines +767 to +769
obj[name] = _Platform_wrapInit(moduleName, exp);
scope['Elm'].hot.__hotReloadData[moduleName] = exp.hotReloadData;
delete exp.hotReloadData;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned earlier, this is where we remove .hotReloadData (from Elm.Main.init for example). Instead it is stored at Elm.hot.__hotReloadData["Main"] for example.

(Note that Elm.hot.__hotReloadData is a flat object. For Elm.Blog.Home the hot reload data is stored at Elm.hot.__hotReloadData["Blog.Home"].)

Comment on lines +795 to +803
var app = init(args);

var hotReload = app.hotReload;
delete app.hotReload;
var reloadFunction = function ()
{
hotReload(scope['Elm'].hot.__hotReloadData[moduleName]);
};
scope['Elm'].hot.__reloadFunctions.push(reloadFunction);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned earlier, this is where .hotReload is removed from app.

Instead we store it in Elm.hot.__reloadFunctions.

Also notice how app.hotReload is called with init.hotReloadData. In essence, this lets a running app get the impl from a new version of the same Elm entrypoint and update itself with it.

Also notice how we read from scope['Elm'].hot all the time. That’s because in _Platform_export__DEBUG we mutate stuff in scope['Elm'].hot when we hot reload.

It’s a bit mind-bend:y, but this allows apps initialized from some earlier version of Elm code have access to the latest stuff from the latest hot reload.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants