Skip to content

Add hot reloading and make it possible to stop Elm apps - #1155

Open
lydell wants to merge 9 commits into
elm:masterfrom
lydell:hot-reload-stop
Open

lydell wants to merge 9 commits into
elm:masterfrom
lydell:hot-reload-stop

Conversation

@lydell

@lydell lydell commented Nov 12, 2025 •

Copy link
Copy Markdown

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. Closes #886 and closes #1125

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:

Closes elm/browser#122 (since I’ve documented how to init apps).

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
Copy Markdown

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 thread javascript-interface.md Outdated
Co-authored-by: Jeroen Engels <jfm.engels@gmail.com>
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
Copy Markdown
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);

@lydell lydell Nov 15, 2025 •

Copy link
Copy Markdown
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 = impl.__$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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/Elm/Kernel/List.js
return _List_fromArray(_List_toArray(xs).sort(function(a, b) {
var ord = A2(f, a, b);
return ord === __Basics_EQ ? 0 : ord === __Basics_LT ? -1 : 1;
return ord.$ === __Basics_EQ.$ ? 0 : ord.$ === __Basics_LT.$ ? -1 : 1;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

kioan000 added a commit to kioan000/elm-toolkit that referenced this pull request Oct 3, 2026
## Why

[elm/core#1155](elm/core#1155) lets a
development build reload itself through `Elm.hot.reload()`, with
companion changes in `elm/browser` and `elm/virtual-dom`. The pull
requests are still open, so this branch is an experiment: it uses them
through the kernel patcher and keeps the elm-hot runtime for projects
that do not patch.

## What changes

- **Hot loader:** it injects both ways and the running code picks one.
With `Elm.hot`, each new version of the module goes to
`Elm.hot.reload()` and the elm-hot runtime stays off; without it, the
elm-hot runtime works as before. The choice happens at runtime, because
an optimized build still contains `Elm.hot` as dead code. When the hot
loader is listed after the Elm loader, it now stops with a message that
says to swap them.
- **Patch archive:** it now holds `elm/core`, `elm/browser` and
`elm/virtual-dom` from the branches of #1155, next to the existing
`elm/html`. There is one patch set: a project either uses the patcher
and gets every patch, or keeps the official kernel.
- **Patches of one's own:** the patcher and the plugin take `patches`, a
`.tar.gz` archive of a `patches/` folder or the folder itself. Such
patches skip the check of the Elm version; the check of each package
version against `elm.json` still applies. Archives are extracted into a
temporary folder, not inside the installed package. Patches in another
layout stop with the expected one, before anything is copied.
- **Elm home:** the patcher, the plugin and the loader take an optional
`elmHome`. Without it, each one uses the `ELM_HOME` of the environment,
or `~/.elm`. A relative path starts from the folder that holds
`elm.json`, or from `cwd` in the loader. To keep the patches to one
project, give the plugin and the loader the same folder.
- **Plugin:** it is renamed `ElmKernelPatcherPlugin`, and `isEnabled`
also accepts a function that receives the webpack compiler.
- **Changelog:** the entries under Unreleased in `CHANGELOG.md` describe
each breaking change with how to fix it.

## Breaking changes

- The plugin class is now `ElmKernelPatcherPlugin`, and its options type
is `ElmKernelPatcherPluginOptions`.
- `useArchive` is removed from the command line, the plugin and the
library; `patches` replaces it.
- `prepareArgs` takes an options object: `prepareArgs({ elmJsonFolder,
elmHome, patches })`.
- Projects that use the plugin now get the patched `elm/core` too, also
in production builds; the hot reloading code is not in a build made with
`--optimize`.

## Checks

- Lint, format and every test suite pass. The tests cover relative and
absolute paths for `elmHome` and `patches`, a patch archive and a patch
folder of one's own, an Elm version outside the bundled ones, missing
patches, and the `isEnabled` function.
- The archive works with Elm 0.19.1 and 0.19.2.
- In a browser with webpack-dev-server, with `elmHome:
'elm-home/elm-stuff'` on the plugin and on the loader: the update goes
through `Elm.hot`, the model keeps its values, and `~/.elm` does not
change. Without the patches, the update goes through the elm-hot
runtime.
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.

Developer experience: Not always clear how to init from JS justification for adding app.kill()

2 participants