Migrate to Fabulous 10.0.x
Fabulous 10 unifies core, MAUI, Avalonia, extensions, templates, samples, and releases in this repository. The NuGet package ID for core remains Fabulous, and public namespaces remain Fabulous; the project and assembly are named Fabulous.Core. All maintained packages now share the 10.0.x release line.
From Fabulous 3 prereleases
- Remove prerelease pins such as
3.0.0-pre22or3.0.0-pre23and reference the same10.0.xversion forFabulous,Fabulous.MauiControlsorFabulous.Avalonia, and any maintained extensions. - Replace references to the former platform repositories with package references or the new source locations under
src/mauiandsrc/avalonia. - Reinstall
Fabulous.MauiControls.TemplatesorFabulous.Avalonia.Templatesat10.0.x, generate a temporary app, and compare project hosts/resources with your app. - Build every target and run update tests plus backend UI tests. Do not infer device compatibility from a neutral build.
Most Fabulous 3 MVU code keeps Program.stateful, statefulWithCmd, or statefulWithCmdMsg; verify tuple shapes against programs and commands. Use the current MAUI and Avalonia samples when an imported prerelease example differs.
From Fabulous 2 or Xamarin.Forms
There is no maintained Xamarin.Forms backend in Fabulous 10. Migrate the host to .NET MAUI or Avalonia and replace Fabulous.XamarinForms imports, XamarinFormsProgram.run, and Xamarin.Forms control types with the chosen backend. Do not mechanically rename namespaces: create a current template app, move the model/update logic first, then rebuild the view using current builders and modifiers.
Replace old View.* constructors with the backend's current open type Fabulous.Maui.View or open type Fabulous.Avalonia.View style. Revisit navigation, styles, platform services, permissions, and ViewRef code because their native APIs changed. The two end-to-end tutorials and UI guide provide current starting points.
Cmd module changes (since 2.5.0-pre8)
If you're coming from Fabulous 2.4.x or earlier, the Cmd module changed substantially in 2.5.0 pre-releases — well before the Fabulous 3 or 10.0.x lines, and never called out at the time as a breaking Cmd API change. If your code predates this, check for the following:
Cmd.ofSub— removed. UseCmd.ofEffectwith a function of the same shape (Dispatch<'msg> -> unit) instead. If you usedCmd.ofSubto set up a long-lived listener or callback (for example an event handler or a timer),Cmd.ofEffectstill works, but you may prefer the currentSubsubscription mechanism, which manages start/stop lifecycle for you.Cmd.dispatch— removed from the public API (the internal equivalent,Cmd.exec, is private and also gained anonErrorhandler parameter).- The old
Sub<'msg>type (Dispatch<'msg> -> unit) — renamed toEffect<'msg>. This is the same type under a new name, not a new type with different semantics. The newCmd.ofEffect : Effect<'msg> -> Cmd<'msg>bridges it intoCmd.Note: this old
Sub<'msg>/Effect<'msg>is unrelated to the currentSubsubscription mechanism (SubId,IDisposable-based lifecycle tracking). The shared name is historical and has caused confusion. Cmd.ofAsyncMsg/Cmd.ofAsyncMsgOption/Cmd.ofTaskMsg— relocated toCmd.OfAsync.msg/Cmd.OfAsync.msgOption/Cmd.OfTask.msg. This is not a pure rename: in 2.4.1 the async work was started on the UI thread, whereas the relocated functions no longer guarantee that. See the threading warning below.Cmd.ofAsyncResult/Cmd.ofTaskResult— removed.Cmd.OfAsync.either/Cmd.OfTask.eitherare the closest equivalents, but take two continuations (ofSuccess,ofError) instead of the previous three (success,errorfor a domainResult.Error,failurefor a thrown exception). If your code relied on that distinction, handle it explicitly — e.g. catch exceptions inside your task function and fold them into yourResultbefore it reacheseither, so a thrown exception and a domain error don't collapse into the same handler.
⚠️ Two changes are likely to cause silent behavioral bugs rather than compile errors:
- Exception handling (
ofAsyncResult/ofTaskResult): a mechanical rename toCmd.OfAsync.eitherwill build successfully but changes what happens when your task throws.- Threading (
ofAsyncMsg/ofAsyncMsgOption/ofTaskMsg): a mechanical rename toCmd.OfAsync.msg/Cmd.OfAsync.msgOption/Cmd.OfTask.msgwill build successfully, but the old guarantee of starting on the UI thread is gone, so the code may now run elsewhere. This typically surfaces at runtime, for example with APIs that must be called on the main thread, such as .NET MAUI permission checks (Permissions.CheckStatusAsync,Permissions.RequestAsync).
Example: keeping a call on the UI thread
If the async body touches UI-thread-affine APIs, marshal that call explicitly instead of relying on the command's starting thread:
// Fabulous 2.4.1 — the async was started on the UI thread
Cmd.ofAsyncMsg
(
async
{
let! status =
Permissions.CheckStatusAsync<Permissions.PostNotifications>()
|> Async.AwaitTask
return PermissionResult (status = PermissionStatus.Granted)
}
)
// Fabulous 10.0.x — marshal the UI-thread-affine call explicitly
Cmd.OfAsync.msg
(
async
{
let! status =
MainThread.InvokeOnMainThreadAsync<PermissionStatus>(fun () ->
Permissions.CheckStatusAsync<Permissions.PostNotifications>())
|> Async.AwaitTask
return PermissionResult (status = PermissionStatus.Granted)
}
)
Note: The explicit type argument (
<PermissionStatus>) is used here to make the overload choice unambiguous.
Finish by removing retired packages and links, clearing bin/obj, restoring, and checking the installed package graph:
dotnet list package --include-transitive
dotnet build -c Release
dotnet test -c Release
Program construction changes (MAUI)
In Fabulous 10.0.x, program construction is split between the core MVU program and the MAUI view layer.
Fabulous 2.4.x vs 10.0.x
Before:
Program.statefulWithCmd init update view
|> Program.withSubscription subscriptions
Now:
Program.statefulWithCmd init update
|> Program.withSubscription subscriptions
|> Fabulous.Maui.Program.withView view
Why
Fabulous 10 separates Program<'arg,'model,'msg> (core MVU logic) from Program<'arg,'model,'msg,'marker> (includes view rendering). UseFabulousApp requires the latter form.
Subscriptions
If chaining multiple subscriptions, consolidate them:
let subscriptions model : Sub<Msg> =
Sub.batch
[
subscriptionA model
subscriptionB model
]
Program construction changes (Avalonia)
This entry is under preparation.
⚠️ Issues during migration
If you run into problems during migration (and you will), please consult the Fabulous community on Discord first (in the F# channel, under the Fabulous project), rather than immediately resorting to LLM-based coding assistants.
Even if you manage to resolve the problem yourself, please share it on Discord. This helps us learn about migration issues that might otherwise never be reported as formal GitHub issues.