flatware
flatware copied to clipboard
Type-safe F# state management (like Elm and Redux) for Blazor
Flatware for Blazor
OBSOLETE! Flatware has been merged with Blazor-Redux, which provides an improved design more like Redux, and supports F# and C# equally.
Flatware is a state management library for Blazor, similar to Elm and Redux. It has the following features:
- Implements a one-way model-update-view architecture, by many considered to be more robust and easier to reason about than a two-way data binding as found in Angular.
- Application state is kept in a single state store. This opens up for advanced features such as undo/redo, hydration of application state, time-traveling debuggers, isomorphic apps with shared .NET code on the frontend and backend, etc.
- Any Blazor component that is upgraded to a Flatware component will subscribe to changes in the state store and automatically update its view, so you don't have to worry about calling
StateHasChanged(). - The view engine is Razor, just like Blazor without Flatware. This combines the power of a templating engine with the familiarity of HTML, like JSX, and is much less alien than the view languages used in Elm and Fable. The Blazor pages themselves become very simple, with just presentational content, references to state in the model, and dispatching of application messages.
- Flatware uses F#, which means you write your model, your application messages and your reducer logic in F#. While this will doubtlessly put off some C# developers, F# has some really useful language features. The discriminated union types are perfect for designing type-safe application messages, and the
withkeyword in record types makes it simple to work with immutable types in your reducer logic. Not to mention that a model with many small types can be created with much less ceremony. F# lends itself well to type driven development. The Blazor project itself and the Razor pages must be C#.
Getting started
-
Assuming you have Visual Studio 15.7 or newer and the Blazor tooling installed, create a new standalone Blazor project.
-
Add a .NET Standard F# class library to the solution.
-
Add a reference from the Blazor project to the F# project.
-
Add the Flatware NuGet package to both projects.
-
In
Library.fs, add your message type, model types, and your component base class with the reducer logic:
open System
open System.Net.Http
open Microsoft.AspNetCore.Blazor
open Microsoft.AspNetCore.Blazor.Components
open FSharp.Control.Tasks
open Flatware
type MyMsg =
| Increment of n : int
| LoadWeather
type WeatherForecast() =
member val Date = DateTime.MinValue with get, set
member val TemperatureC = 0 with get, set
member val TemperatureF = 0 with get, set
member val Summary = "" with get, set
type MyMdl = { Count : int; Forecasts : WeatherForecast list } with
static member Init = { Count = 0; Forecasts = [] }
type MyAppComponent() =
inherit FlatwareComponent<MyMsg, MyMdl>()
[<Inject>]
member val Http = null : HttpClient with get, set
override this.ReduceAsync(msg : MyMsg, mdl : MyMdl) =
task {
match msg with
| Increment n ->
return { mdl with Count = mdl.Count + n }
| LoadWeather ->
let! forecasts = this.Http.GetJsonAsync<WeatherForecast[]>("/sample-data/weather.json") |> Async.AwaitTask
return { mdl with Forecasts = Array.toList forecasts }
}
- Open
Program.csand configure Flatware in theBrowserServiceProvider:
configure.AddFlatware<MyMsg, MyMdl>(MyMdl.Init);
You will need to add
using Flatware;
using ClassLibrary1;
at the top, assuming your F# library was called ClassLibrary1.
- The architecture is now ready for use in your Blazor pages. Open
Counter.cshtml. Remove the entire@functionsblock. Change the header to:
@page "/counter"
@inherits MyAppComponent
@using ClassLibrary1
Replace
<p>Current count: @currentCount</p>
with
<p>Current count: @Mdl.Count</p>
Also replace
<button @onclick(IncrementCount)>Click me</button>
with
<button @onclick(() => DispatchAsync(MyMsg.NewIncrement(3)))>Click me</button>
- In
FetchData.cshtml, do the same change to the header and remove the@functionsblock.
Replace both occurrences of forecasts with Mdl.Forecasts.
- All that remains is that the application message
LoadWeatherneeds to be dispatched from somewhere. It could be from a button, or it could be from anOnInitAsync()method in the Blazor page. But it seems more natural to load the weather data when the application starts, which is why we'll changeApp.cshtmlto the following:
@inherits MyAppComponent
@using ClassLibrary1
<!--
Configuring this here is temporary. Later we'll move the app config
into Program.cs, and it won't be necessary to specify AppAssembly.
-->
<Router AppAssembly=typeof(Program).Assembly />
@functions
{
protected override async Task OnInitAsync()
{
await this.DispatchAsync(MyMsg.LoadWeather);
}
}
Contributing
Flatware is definitely experimental at the moment, and you should expect breaking changes. But I'd be very interested in discussing the design and potential features. Please open an issue if you have any particular topic in mind.