Skip to main content

Announcing Elmish Land 2.0 Preview

· 5 min read
Kristofer Löfberg
Elmish Land creator

Version 2.0 brings Elmish Land onto Fable 5 (announcement) and the matching ecosystem majors — Feliz 3, Fable.Core 5, Fable.Elmish 5 etc. New projects created with init get these versions out of the box, and a new upgrade command moves existing projects across in one shot.

This is a major version bump because the underlying libraries contain breaking changes. Most Elmish Land applications will compile after running upgrade, but some will need code edits.

Before you run upgrade on a real project, read the Migrating from 1.x to 2.0 guide — it covers the .NET 10 prerequisites you have to set up manually, exactly which files upgrade rewrites, and the Feliz 3 code patterns you need to fix by hand.

Fable 5 by default

The Fable dotnet tool installed by dotnet elmish-land init is now version 5. This is a major step forward for the F# → JavaScript compiler that powers every Elmish Land app. Existing 1.x projects were on Fable 4; 2.0 makes Fable 5 the default for new projects and gives existing projects a one-command path to move over.

Updated package dependecies

Fable 5 lands together with several aligned ecosystem upgrades:

PackageMajor
fable (dotnet tool)5
FSharp.Core10
Fable.Core5
Fable.Promise3
Fable.Elmish5
Fable.Elmish.HMR9
Fable.Elmish.React5
Feliz3
Feliz.RouterRemoved; vendored at .elmish-land/Base/Router.fs
react / react-dom19
vite8

init and upgrade resolves each major to the latest published patch at the moment you run it (see Dynamic version resolution below). The major bumps were upgraded together because they're all part of the same release wave. See the migration guide for the complete list of files upgrade touches.

URL change handling moved to a subscription

As part of the upgrade, the scaffold's URL-change handling has been reworked. Where 1.1 wrapped the entire view tree in a React.router element, 2.0 uses an Elmish subscription:

let private urlChangeSubscription _model =
let routeMode = Feliz.Router.RouteMode.Hash // or .Path, based on elmish-land.json
[ [ "ElmishLand"; "UrlChanges" ],
fun dispatch ->
Feliz.Router.Router.subscribeToUrlChanges
routeMode
(Route.parse >> RouteChanged >> dispatch) ]

let subscribe model =
Sub.batch [
Sub.map "Shared" SharedMsg (Shared.subscriptions model.Shared)
urlChangeSubscription model
// ...
]

Two practical consequences:

  • Your view is no longer nested inside an extra router element, which keeps the rendered React tree closer to what you wrote.
  • A small vendored copy of Feliz Router lives at .elmish-land/Base/Router.fs. It's regenerated by init, upgrade, and restore, so you should not edit it by hand. The Feliz.Router NuGet dependency has been removed from the scaffold, and upgrade strips any leftover Feliz.Router PackageVersion entry from Directory.Packages.props and any PackageReference entry from your .fsproj files.

If you wrote any code that assumed the view was rendered inside a React.router, you'll want to revisit it after upgrading.

Breaking changes you may hit

Most code edits land in Feliz 3 territory: a handful of APIs that used to do extra work behind the scenes now require an explicit second call. The patterns to watch for are React.memo (now paired with React.memoRender), React.lazy' (paired with React.lazyRender), the redesigned React.context API, hand-written bindings using Interop.reactApi.createElement (now ReactLegacy.createElement), several helpers that moved into the FsReact namespace, and built-in component values that are now PascalCase (React.Fragment, React.Suspense, …).

Fable 5, vite 8, and Fable.Elmish.HMR 9 also ship their own breaking changes — most notably the FABLE_COMPILER_4FABLE_COMPILER_5 rename, vite's switch from Rollup to Rolldown, and raised default browser targets.

The migration guide walks through each Feliz 3 pattern with before/after examples and links to the upstream changelogs for the rest.

Dynamic version resolution during init and upgrade

Previously, every scaffolded Directory.Packages.props and package.json had hardcoded versions baked into the CLI. That meant a new patch release of Feliz or vite required a new Elmish Land release before init would pick it up.

In 2.0, init resolves versions dynamically. The CLI keeps a list of pinned majors (Feliz 3, vite 8, Fable 5, …) and asks the NuGet and npm registries for the latest version matching each major at the moment you run init. New projects always start on the freshest available patch and minor releases.

Upgrading an existing project: the new upgrade command

The migration story for existing projects is a single command:

dotnet tool update elmish-land --prerelease
dotnet elmish-land upgrade

upgrade rewrites Directory.Packages.props, package.json, .config/dotnet-tools.json, and the regenerated files under .elmish-land/ so the project lands on the 2.0 dependency set. It does not edit global.json or your project files' <TargetFramework> — those .NET 10 prerequisites you set up by hand first. The migration guide lays out the full sequence, including back-up advice and what upgrade leaves alone.

Quality of life fixes

  • The interactive route-mode prompt during init no longer fights with the spinner — the prompt is shown first, then the spinner takes over for the actual scaffolding.
  • The generated routing code no longer triggers F# warnings about unused query-parameter bindings on routes that don't declare any query parameters.
  • The generated sharedCommandToCmd function no longer triggers a warning about an unused msg binding.

Getting 2.0

If you already have Elmish Land installed:

dotnet tool update elmish-land --prerelease
dotnet elmish-land upgrade

Or for a fresh start:

dotnet tool install elmish-land --prerelease
dotnet elmish-land init

Elmish Land is a framework for building F# browser apps with Fable, React, and Elmish. GitHub repository