MVU Pattern
An architecture pattern that represents screen state as one value, updates it with a pure function that receives messages, and renders it with a view function.
· Last updated: 2026-10-26 · 25 min read
Overview
Suppose a counter screen has +, - and reset buttons. A common implementation gives each button an event handler that directly changes a count variable and updates a label. Three handlers mean three places that change state and three that change the screen. If the reset handler assigns only count = 0; and forgets the label, the state becomes 0 while the screen still shows 2. With no record of which buttons were pressed in which order, reproducing the wrong screen requires someone to repeat the clicks.
MVU (Model-View-Update) gives state changes a single route. The model is one value containing all the state the screen needs. Here it is a single counter integer. A message, called Msg in the code, represents something that can happen as data. A button never directly changes count; it sends a message such as Increment. Defining a closed set of message kinds also limits the reasons state can change to that set.
Update is a function that receives the current model and a message and returns the next model. It creates a new value rather than modifying the existing model, and stays pure: the same inputs always produce the same result, without reading or changing external state. Handling reset becomes one line, Reset => model with { Count = 0 }. The view is a function that creates a screen from the model alone. Instead of writing screen updates in each handler, it runs again whenever a new model is produced, reducing the risk of a handler changing state but forgetting to update the screen.
Here, the component that receives messages and calls these two functions is called the runtime. It keeps the current model in one place. On receiving a message, it gets a new model from update, stores it and calls view again. In Elm, the language runtime does this, and buttons carry the messages to send when clicked.1 In other languages, a framework or a small custom loop takes this role.
Structure
Runtime holds Model as a field and calls Counter's two static functions, shown underlined. Update knows only Model and Msg, with no knowledge of Runtime. Increment, Decrement, Reset are subtypes of the abstract record Msg.
One click corresponds to one Dispatch call. The runtime obtains a new model from update and calls view with it to rebuild the screen string; the caller then reads Screen.
History
MVU originated in Elm. The official Elm guide calls it The Elm Architecture and describes three parts: model, view and update. According to the guide, it emerged as early Elm programmers repeatedly found the same basic pattern in their code, rather than through a single invention.2
Elm was created by Evan Czaplicki. His March 30, 2012 thesis, "Elm: Concurrent FRP for Functional GUIs", introduced Elm as a concurrent FRP language for making responsive GUIs easier to build.3 FRP, functional reactive programming, deals with values that change over time. This was his undergraduate thesis at Harvard University.4 Its foldp combines each value arriving from an input signal, a value changing over time, with an accumulator. The thesis says to read the name as "fold from the past", like a list's foldl.5
The README in Czaplicki's elm-architecture-tutorial repository, published in January 2015, stated that every Elm program's logic divides neatly into model, update and view, presenting a update : Action -> Model -> Model skeleton. Its counter example built the model using Signal.foldp update 0 (...). The model resulted from folding incoming action signals with update, and today's Msg was then called Action.6
The May 2016 Elm 0.17 announcement, "A Farewell to FRP", introduced subscriptions. A subscription is a value registering interest in messages from external events such as the current time or WebSocket messages. The article said that all signal-related concepts had been replaced with simpler ones. As The Elm Architecture took hold, it had become clear that nearly all Elm programs could be written without thinking about signals.7 In the current guide, a program sends commands (Cmd) and subscriptions (Sub) to the runtime alongside its screen; the runtime performs effects such as HTTP requests.8 For a program with effects, update has the type Msg -> Model -> (Model, Cmd Msg), and command results arrive later as messages sent back to update.9 The November 2016 Elm 0.18 announcement introduced exporting and importing the exact event sequence received by a program in debug mode.10
Redux's Prior Art page says Elm enforces a "model view update" architecture and that its updater serves the same role as a Redux reducer.11 A reducer is a function that takes the current state and an action and returns a new state. The Elm guide also notes that The Elm Architecture inspired projects such as Redux.2 Redux 1.0.0 was released on August 14, 2015.12
In .NET, the F# framework Fabulous combines declarative UI and MVU to build mobile and desktop applications on .NET MAUI or Avalonia.13 The May 2020 .NET MAUI announcement listed MVU in C# as a future feature alongside existing MVVM and XAML, describing MVU as encouraging one-way data flow and state management.14 The C# UI toolkit Comet also described itself as following MVU on .NET MAUI, but its GitHub repository is archived.15
Timeline
Year | Event | Source |
|---|---|---|
2012 | Evan Czaplicki's thesis "Elm: Concurrent FRP for Functional GUIs", dated March 30, described | |
2015 | The elm-architecture-tutorial README presented a program skeleton divided into model, update and view in January | |
2015 | Redux 1.0.0 was released on August 14 | |
2016 | Elm 0.17 introduced subscriptions and replaced signals: "A Farewell to FRP", May 10 | |
2016 | Elm 0.18 introduced exporting and importing event histories in debug mode on November 14 | |
2020 | The .NET MAUI announcement mentioned C# MVU as a future feature on May 19 |
- Year
2012
- Event
Evan Czaplicki's thesis "Elm: Concurrent FRP for Functional GUIs", dated March 30, described
foldpas "fold from the past"- Source
- Year
2015
- Event
The elm-architecture-tutorial README presented a program skeleton divided into model, update and view in January
- Source
- Year
2015
- Event
Redux 1.0.0 was released on August 14
- Source
- Year
2016
- Event
Elm 0.17 introduced subscriptions and replaced signals: "A Farewell to FRP", May 10
- Source
- Year
2016
- Event
Elm 0.18 introduced exporting and importing event histories in debug mode on November 14
- Source
- Year
2020
- Event
The .NET MAUI announcement mentioned C# MVU as a future feature on May 19
- Source
Why use it?
With update as the sole route for state changes, the message history and update function explain how a state arose. Update and view are pure functions, so tests can compare Update(model, msg) results without opening a UI framework.
Update's shape, (model, message) -> model (model and message to model), matches a fold's combining function. Starting from the initial model and folding received messages in order produces the current model. Recording messages therefore allows the same state to be reconstructed by folding them in the same order. When update also returns commands, replay folds only the model part of each result and does not execute commands. The extended example checks this replay. The argument order follows C#'s Aggregate; Elm's update : Msg -> Model -> Model takes the message first, matching foldp's (a -> b -> b) order.
MVU also represents effects as data. Update never saves data or sends HTTP requests directly; it returns a command value, such as "save this list". The runtime executes it, and its result returns as another message. Testing update needs no fake repository: a test can simply check that the returned command has the expected value. Redux likewise requires reducers to avoid asynchronous logic and side effects.16
Unless a language enforces it, update's purity remains a convention. The Redux documentation distinguishes Elm on this point: as a language, Elm provides enforced purity, static types, immutability and pattern matching.11 C# will compile an update function that writes a file or reads the current time, but doing either prevents recorded messages from reliably recreating the same state. When a time or random value is needed, the runtime obtains it and includes it in a message. C# also lacks closed sum types, so adding a message kind without an update branch need not produce a compiler warning.
A single model means the model and message set grow with the screen. Splitting sub-screens into models and message sets and delegating from a parent update to child updates requires explicit design, with delegation code repeated for each screen. View recalculates the screen from the full model; the framework's renderer, rather than the pattern, applies only changed parts to the actual UI. The elm/browser 1.0.2 runtime generally defers drawing to the next animation frame, calls view once for several messages within that frame, then compares the new virtual DOM with the previous one to update only changed parts of the real DOM.17 Html.Lazy can even skip building virtual nodes for parts whose function and arguments remain unchanged.18 For screens with almost no state, or frameworks designed around two-way binding (MVVM Pattern), defining messages and adding a runtime may require more work than the separation warrants.
Example
A counter reduces state to one integer, keeping attention on receiving a message, calculating a new model and creating a screen from it. This example uses the overview's counter, sending a message array instead of clicking buttons and printing the screen string produced for each message. Msg is an abstract record. Because C# has no closed sum types, a switch expression issues CS8509 even when every known subtype is handled, so a final _ branch is included.
Starting from 0, sending Increment, Increment, Decrement, and Reset produces models 1, 2, 1, and 0. Update calculates the result of each message, while the runtime calls view to rebuild its screen string. The output after the final Reset shows why reset handling needs no separate label update.
using System;
using System.Collections.Generic;
using System.Linq;
var runtime = new Runtime(new Model(0));
Console.WriteLine($"{"start",-9} -> {runtime.Screen}");
// Send a predefined message list in order instead of clicking buttons.
Msg[] clicks = [new Increment(), new Increment(), new Decrement(), new Reset()];
foreach (Msg msg in clicks)
{
runtime.Dispatch(msg);
Console.WriteLine($"{msg.GetType().Name,-9} -> {runtime.Screen}");
}
// Model: an immutable value containing all state needed by the screen.
sealed record Model(int Count);
// Messages: the set of events that can happen on the screen.
abstract record Msg;
sealed record Increment : Msg;
sealed record Decrement : Msg;
sealed record Reset : Msg;
static class Counter
{
// Update: calculate the next model from only the current model and message.
public static Model Update(Model model, Msg msg) => msg switch
{
Increment => model with { Count = model.Count + 1 },
Decrement => model with { Count = model.Count - 1 },
Reset => model with { Count = 0 },
// C# has no closed sum types; CS8509 remains even with all subtypes handled, so include a final branch.
_ => throw new ArgumentOutOfRangeException(nameof(msg), msg, "unknown message"),
};
// View: create the screen using only the model.
public static string View(Model model) => $"[ - ] {model.Count} [ + ]";
}
// Runtime: keep the model in one place and call Update followed by View for each message.
sealed class Runtime(Model initial)
{
private Model model = initial;
public string Screen { get; private set; } = Counter.View(initial);
public void Dispatch(Msg msg)
{
model = Counter.Update(model, msg);
Screen = Counter.View(model);
}
}start -> [ - ] 0 [ + ]
Increment -> [ - ] 1 [ + ]
Increment -> [ - ] 2 [ + ]
Decrement -> [ - ] 1 [ + ]
Reset -> [ - ] 0 [ + ]Reset handling changes only the model. The runtime keeps the screen synchronized by calling View again for each message.
Extended example
After the counter's state calculation, a to-do list with saving shows how to separate external work from update. Saving is an effect that may fail, so update does not perform it directly. Instead, it returns a (Model, Cmd) tuple. Cmd is data describing the effect the runtime should perform; here, SaveTodos carries the items to save as a value. The runtime queues commands, executes them in RunEffects(), and translates results into Saved or SaveFailed messages sent back to update. The fake repository is configured to fail on the first save and succeed on the second.
Invalid messages are included too: a whitespace-only title in Add, a nonexistent ID in Toggle, and a save in progress when receiving Save. None throws; each leaves a reason in the model's Note. Undo is added to Msg without a corresponding update branch. Finally, the runtime's recorded messages are folded using Aggregate from the initial model to check that the same model is produced without calling the repository. Record list fields compare by reference, so items are checked with SequenceEqual and the remaining fields with record comparison.
The saves deliberately fail and then succeed to distinguish the state when a button is pressed from the state after external work finishes. A save request first produces a saving model and a command; only the resulting message moves it to a failed or completed state. The second save is a new Save message. Replay processes these result messages too, without executing save commands, so reconstructing screen state remains separate from saving again.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
// Fake repository: the first save fails and the second succeeds.
var store = new FakeStore([false, true]);
var runtime = new Runtime(Todo.Init, store.Run);
runtime.Dispatch(new Add("buy milk"));
runtime.Dispatch(new Add(" "));
runtime.Dispatch(new Add("write report"));
runtime.Dispatch(new Toggle(1));
runtime.Dispatch(new Toggle(42));
runtime.Dispatch(new Save());
runtime.Dispatch(new Save());
runtime.RunEffects();
runtime.Dispatch(new Save());
runtime.RunEffects();
runtime.Dispatch(new Undo());
Console.WriteLine($"log: {runtime.Log.Count} messages, store calls: {store.Calls}");
// Fold recorded messages from the initial model without executing commands.
Model replayed = runtime.Log.Aggregate(Todo.Init, (model, msg) => Todo.Update(model, msg).Model);
Console.WriteLine($"replay: {Todo.View(replayed)}");
// List fields compare by reference: use SequenceEqual for items, then align Items and compare the remaining record fields.
bool same = replayed.Items.SequenceEqual(runtime.Model.Items) && replayed with { Items = runtime.Model.Items } == runtime.Model;
Console.WriteLine($"same model: {same}, store calls: {store.Calls}");
sealed record TodoItem(int Id, string Title, bool Done);
sealed record Model(ImmutableList<TodoItem> Items, int NextId, bool Saving, string Note);
// Both user actions and effect results are messages.
abstract record Msg;
sealed record Add(string Title) : Msg;
sealed record Toggle(int Id) : Msg;
sealed record Save : Msg;
sealed record Saved(int Count) : Msg;
sealed record SaveFailed(string Reason) : Msg;
// A message added to Msg without a corresponding Update branch.
sealed record Undo : Msg;
// Command: data describing an effect for the runtime. Update does not execute it.
abstract record Cmd;
sealed record NoCmd : Cmd;
sealed record SaveTodos(ImmutableList<TodoItem> Items) : Cmd;
static class Todo
{
public static readonly Model Init = new([], 1, false, "");
static readonly Cmd None = new NoCmd();
public static (Model Model, Cmd Cmd) Update(Model model, Msg msg) => msg switch
{
Add a when string.IsNullOrWhiteSpace(a.Title) => (model with { Note = "title is empty" }, None),
Add a => (model with
{
Items = model.Items.Add(new TodoItem(model.NextId, a.Title, false)),
NextId = model.NextId + 1,
Note = "",
}, None),
Toggle t when !model.Items.Any(i => i.Id == t.Id) => (model with { Note = $"no todo #{t.Id}" }, None),
Toggle t => (model with
{
Items = model.Items.Select(i => i.Id == t.Id ? i with { Done = !i.Done } : i).ToImmutableList(),
Note = "",
}, None),
// A save requested while saving produces no command.
Save when model.Saving => (model with { Note = "save already running" }, None),
Save => (model with { Saving = true, Note = "" }, new SaveTodos(model.Items)),
Saved s => (model with { Saving = false, Note = $"saved {s.Count}" }, None),
SaveFailed f => (model with { Saving = false, Note = $"save failed: {f.Reason}" }, None),
// A message without a handling branch leaves a note without changing items.
_ => (model with { Note = $"unhandled {msg.GetType().Name}" }, None),
};
public static string View(Model model)
{
string items = string.Join(", ", model.Items.Select(i => $"#{i.Id}[{(i.Done ? "x" : " ")}] {i.Title}"));
string state = model.Saving ? "saving" : "idle";
string note = model.Note == "" ? "" : $" | {model.Note}";
return $"{items} | {state}{note}";
}
}
// Runtime: record messages, call Update and queue returned commands for later execution.
sealed class Runtime(Model initial, Func<Cmd, Msg> execute)
{
private readonly List<Msg> log = [];
private readonly Queue<Cmd> pending = new();
public Model Model { get; private set; } = initial;
public IReadOnlyList<Msg> Log => log;
public void Dispatch(Msg msg)
{
log.Add(msg);
(Model, Cmd cmd) = Todo.Update(Model, msg);
Console.WriteLine($"{msg} -> {Todo.View(Model)}");
if (cmd is not NoCmd) pending.Enqueue(cmd);
}
// Effect results return through Update as new messages.
public void RunEffects()
{
while (pending.TryDequeue(out Cmd? cmd)) Dispatch(execute(cmd));
}
}
sealed class FakeStore(bool[] outcomes)
{
public int Calls { get; private set; }
public Msg Run(Cmd cmd)
{
if (cmd is not SaveTodos save) return new SaveFailed($"unknown command {cmd.GetType().Name}");
bool ok = outcomes[Calls];
Calls++;
Console.WriteLine($" effect: save {save.Items.Count} items, call {Calls}");
return ok ? new Saved(save.Items.Count) : new SaveFailed("disk full");
}
}Add { Title = buy milk } -> #1[ ] buy milk | idle
Add { Title = } -> #1[ ] buy milk | idle | title is empty
Add { Title = write report } -> #1[ ] buy milk, #2[ ] write report | idle
Toggle { Id = 1 } -> #1[x] buy milk, #2[ ] write report | idle
Toggle { Id = 42 } -> #1[x] buy milk, #2[ ] write report | idle | no todo #42
Save { } -> #1[x] buy milk, #2[ ] write report | saving
Save { } -> #1[x] buy milk, #2[ ] write report | saving | save already running
effect: save 2 items, call 1
SaveFailed { Reason = disk full } -> #1[x] buy milk, #2[ ] write report | idle | save failed: disk full
Save { } -> #1[x] buy milk, #2[ ] write report | saving
effect: save 2 items, call 2
Saved { Count = 2 } -> #1[x] buy milk, #2[ ] write report | idle | saved 2
Undo { } -> #1[x] buy milk, #2[ ] write report | idle | unhandled Undo
log: 11 messages, store calls: 2
replay: #1[x] buy milk, #2[ ] write report | idle | unhandled Undo
same model: True, store calls: 2Replay reconstructs the same state because effect results were also recorded as SaveFailed, Saved messages. Commands are not executed during replay, so repository calls remain at 2; executing commands again would save again. SaveTodos captures the list when Save arrives, so changing items while saving can make the saved data differ from the screen. Undo falls into the _ branch and leaves only a note; the C# compiler did not warn of this missing branch. The runtime executes commands sequentially on one thread and does not implement cancellation, timeouts or retries for real asynchronous work.
TypeScript example
The TypeScript version of the same to-do list omits the nonexistent-ID Toggle and Undo cases. Messages and commands are discriminated unions, distinguished by their kind field. When switch handles every kind, the default branch narrows msg to never. Omitting a branch therefore makes the assertNever(msg) call a compile error, so a missing case such as the C# example's Undo cannot compile here. Replay uses the array's reduce, and the two complete models are compared as JSON strings.
The TypeScript version omits C#'s nonexistent-ID change and Undo cases while retaining save failure, a new request, success and replay. Finding a missing message branch at compile time and recovering the same model from a recorded history are separate checks. The former checks whether code handles each message; the latter checks whether the chosen update function calculates the same state from the same input list.
type TodoItem = { readonly id: number; readonly title: string; readonly done: boolean };
type Model = {
readonly items: readonly TodoItem[];
readonly nextId: number;
readonly saving: boolean;
readonly note: string;
};
// Messages and commands are discriminated unions identified by their kind values.
type Msg =
| { kind: "add"; title: string }
| { kind: "toggle"; id: number }
| { kind: "save" }
| { kind: "saved"; count: number }
| { kind: "saveFailed"; reason: string };
type Cmd = { kind: "none" } | { kind: "saveTodos"; items: readonly TodoItem[] };
const init: Model = { items: [], nextId: 1, saving: false, note: "" };
const none: Cmd = { kind: "none" };
function update(model: Model, msg: Msg): [Model, Cmd] {
switch (msg.kind) {
case "add":
if (msg.title.trim() === "") return [{ ...model, note: "title is empty" }, none];
return [{
...model,
items: [...model.items, { id: model.nextId, title: msg.title, done: false }],
nextId: model.nextId + 1,
note: "",
}, none];
case "toggle":
if (!model.items.some((i) => i.id === msg.id)) return [{ ...model, note: `no todo #${msg.id}` }, none];
return [{ ...model, items: model.items.map((i) => (i.id === msg.id ? { ...i, done: !i.done } : i)), note: "" }, none];
case "save":
// A save requested while saving produces no command.
if (model.saving) return [{ ...model, note: "save already running" }, none];
return [{ ...model, saving: true, note: "" }, { kind: "saveTodos", items: model.items }];
case "saved":
return [{ ...model, saving: false, note: `saved ${msg.count}` }, none];
case "saveFailed":
return [{ ...model, saving: false, note: `save failed: ${msg.reason}` }, none];
default:
// Handling every kind narrows msg to never. A missing branch causes a compile error here.
return assertNever(msg);
}
}
function assertNever(value: never): never {
throw new Error(`unhandled message ${JSON.stringify(value)}`);
}
function view(model: Model): string {
const items = model.items.map((i) => `#${i.id}[${i.done ? "x" : " "}] ${i.title}`).join(", ");
const note = model.note === "" ? "" : ` | ${model.note}`;
return `${items} | ${model.saving ? "saving" : "idle"}${note}`;
}
// Fake repository: the first save fails and the second succeeds.
class FakeStore {
calls = 0;
constructor(private readonly outcomes: readonly boolean[]) {}
run(cmd: Cmd): Msg {
if (cmd.kind !== "saveTodos") return { kind: "saveFailed", reason: `unknown command ${cmd.kind}` };
const ok = this.outcomes[this.calls];
this.calls++;
console.log(` effect: save ${cmd.items.length} items, call ${this.calls}`);
return ok ? { kind: "saved", count: cmd.items.length } : { kind: "saveFailed", reason: "disk full" };
}
}
// Runtime: record messages, call update and queue returned commands for later execution.
class Runtime {
readonly log: Msg[] = [];
private readonly pending: Cmd[] = [];
constructor(public model: Model, private readonly execute: (cmd: Cmd) => Msg) {}
dispatch(msg: Msg): void {
this.log.push(msg);
const [next, cmd] = update(this.model, msg);
this.model = next;
console.log(`${JSON.stringify(msg)} -> ${view(next)}`);
if (cmd.kind !== "none") this.pending.push(cmd);
}
// Effect results return through update as new messages.
runEffects(): void {
for (let cmd = this.pending.shift(); cmd !== undefined; cmd = this.pending.shift()) {
this.dispatch(this.execute(cmd));
}
}
}
const store = new FakeStore([false, true]);
const runtime = new Runtime(init, (cmd) => store.run(cmd));
runtime.dispatch({ kind: "add", title: "buy milk" });
runtime.dispatch({ kind: "add", title: " " });
runtime.dispatch({ kind: "add", title: "write report" });
runtime.dispatch({ kind: "toggle", id: 1 });
runtime.dispatch({ kind: "save" });
runtime.dispatch({ kind: "save" });
runtime.runEffects();
runtime.dispatch({ kind: "save" });
runtime.runEffects();
console.log(`log: ${runtime.log.length} messages, store calls: ${store.calls}`);
// Fold recorded messages from the initial model without executing commands.
const replayed = runtime.log.reduce((model, msg) => update(model, msg)[0], init);
console.log(`replay: ${view(replayed)}`);
// Both models use the same key order, so compare their complete JSON strings.
console.log(`same model: ${JSON.stringify(replayed) === JSON.stringify(runtime.model)}, store calls: ${store.calls}`);{"kind":"add","title":"buy milk"} -> #1[ ] buy milk | idle
{"kind":"add","title":" "} -> #1[ ] buy milk | idle | title is empty
{"kind":"add","title":"write report"} -> #1[ ] buy milk, #2[ ] write report | idle
{"kind":"toggle","id":1} -> #1[x] buy milk, #2[ ] write report | idle
{"kind":"save"} -> #1[x] buy milk, #2[ ] write report | saving
{"kind":"save"} -> #1[x] buy milk, #2[ ] write report | saving | save already running
effect: save 2 items, call 1
{"kind":"saveFailed","reason":"disk full"} -> #1[x] buy milk, #2[ ] write report | idle | save failed: disk full
{"kind":"save"} -> #1[x] buy milk, #2[ ] write report | saving
effect: save 2 items, call 2
{"kind":"saved","count":2} -> #1[x] buy milk, #2[ ] write report | idle | saved 2
log: 9 messages, store calls: 2
replay: #1[x] buy milk, #2[ ] write report | idle | saved 2
same model: true, store calls: 2The "saved" branch removal makes tsc emit TS2345 because its never parameter cannot receive { kind: "saved"; count: number; }. As in C#, replay excludes commands, and execution runs sequentially on one thread.
See also
Each C# example is an independent .NET 10 console program.
The TypeScript example runs in Node.js after compilation to JavaScript targeting ES2022 or later.
Footnotes
- Elm Guide. Buttons. The onClick handler and the order in which update and view are called. ↩
- Elm Guide. The Elm Architecture. ↩
- Evan Czaplicki. "Elm: Concurrent FRP for Functional GUIs" (2012). PDF. Title page; Abstract. ↩
- Harvard SEAS. Alumni profile: Evan Czaplicki, A.B. '12 (2015-10-02): The profile describes Elm development leading to a senior thesis project with Stephen Chong. The thesis acknowledgments also identify Stephen Chong as the adviser. Evan Czaplicki. "Elm: Concurrent FRP for Functional GUIs" (2012). PDF. Acknowledgments. ↩
- Evan Czaplicki. "Elm: Concurrent FRP for Functional GUIs" (2012). PDF. Section 3.1.3, Past-dependent Transformation. ↩
- Evan Czaplicki. "A Modular Architecture for Elm programs", elm-architecture-tutorial README (2015). GitHub, commit 570709a. The Basic Pattern; Aside: Driving your App with Signals. ↩
- Evan Czaplicki. A Farewell to FRP (2016-05-10). What are subscriptions? ↩
- Elm Guide. Commands and Subscriptions. ↩
- Elm Guide. HTTP. init, update. ↩
- Evan Czaplicki. The Perfect Bug Report (2016-11-14). ↩
- Redux. Prior Art. Elm. ↩
- reduxjs/redux. v1.0.0 release (2015-08-14). ↩
- fabulous-dev. Fabulous. README. ↩
- Scott Hunter. Introducing .NET Multi-platform App UI. .NET Blog (2020-05-19). ↩
- dotnet. Comet. README. ↩
- Redux. Redux Fundamentals, Part 3: State, Actions, and Reducers. Rules of Reducers. ↩
- elm/browser. src/Elm/Kernel/Browser.js, 1.0.2. BrowsermakeAnimator, Browser.element. ↩
- Elm Guide. Html.Lazy. ↩