What's new in 2.0
Version 2.0 of Elmish Land is here! After two beta releases since May 2026, the framework now runs on Fable 5, Feliz 3, vite 8, and the .NET 10 SDK. New projects get the whole stack out of the box, and existing projects move over with a single upgrade command.
This is a major version bump because the underlying libraries contain breaking changes. Most applications compile after running upgrade, but some will need code edits. Before you upgrade a real project, read the Migrating from 1.x to 2.0 guide — it covers the .NET 10 prerequisites you set up by hand, exactly which files upgrade rewrites, and the Feliz 3 patterns you fix yourself.
New requirements
- .NET SDK 10.0 or higher — up from 6.0 in 1.x. This is a hard requirement: the 2.0 tool won't install on an older active SDK.
- Node.js 20.19 or above — required by vite 8.
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 dependencies
Fable 5 lands together with several aligned ecosystem upgrades:
| Package | 1.1 | 2.0 |
|---|---|---|
fable (dotnet tool) | 4 | 5 |
FSharp.Core | 10 | 10 |
Fable.Core | (implicit) | 5 |
Fable.Promise | 3 | 3 |
Fable.Elmish | 5 | 5 |
Fable.Elmish.HMR | 8 | 9 |
Fable.Elmish.React | 5 | 5 |
Feliz | 2 | 3 |
Feliz.Router | 4 | Removed; vendored at .elmish-land/Base/Router.fs |
react / react-dom | 19 | 19 |
vite | 7 | 8 |
init and upgrade resolve each major to the latest published patch at the moment you run them (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
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 byinit,upgrade, andrestore, so don't edit it by hand. TheFeliz.RouterNuGet dependency has been removed from the scaffold, andupgradestrips any leftoverFeliz.RouterPackageVersionentry fromDirectory.Packages.propsand anyPackageReferenceentry from your.fsprojfiles.
If you wrote code that assumed the view was rendered inside a React.router, 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.memoRenderer), 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_4 → FABLE_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.
Upgrading an existing project: the new upgrade command
The migration story for existing projects is a single command:
dotnet tool update elmish-land
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 — preserving any packages and tools you added yourself. It applies the mechanical Feliz 2 → 3 renames across your .fs files, then prints a checklist of the migrations it can't do for you, each with a deep link into the Feliz upgrade docs. If your solution root contains several Elmish Land apps, one invocation upgrades them all.
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.
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, versions are resolved 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 or upgrade. New and upgraded projects always start on the freshest available patch and minor releases.
Bug fixes
- Commands returned by a layout's
routeChanged(orinit) are no longer dispatched to the page twice when navigating between sibling pages that share the same layout - Projects whose name contains a namespace, e.g.
MyOrg.MyProject, now build (#42) - The SDK version check no longer fails when the latest installed .NET SDK is a preview release (#40)
- The interactive route-mode prompt during
initno 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
sharedCommandToCmdfunction no longer triggers a warning about an unusedmsgbinding
Getting 2.0
If you already have Elmish Land installed:
dotnet tool update elmish-land
dotnet elmish-land upgrade
Or for a fresh start:
dotnet tool install elmish-land
dotnet elmish-land init
Elmish Land is a framework for building F# browser apps with Fable, React, and Elmish. GitHub repository