keepcalculator

06 Numbers and Powers

Blazor Forms and Immutable State for Stable Drafts

Card No. 06·38058 · Updated · Standard formula, shown below

The calculator on this card is switched off while it is checked. The formula is below, and the worked example shows how to do the sum by hand.

Yes, using immutable state in Blazor forms keeps a stable draft while editing, because each change creates a new state object instead of mutating the existing one, so the draft remains consistent and undoable. This article explains how to implement this pattern, why it works, and what to watch out for.

Formula

The calculator above simply counts the characters in the draft text and subtracts that from the maximum length. The formulas are:

  • Current length = number of characters in the draft text (including spaces and punctuation).
  • Remaining = max(0, Max length − Current length).

Variables: t = draft text (string), n = maximum allowed length (integer). The result is shown in characters, which is the standard unit for text length in most systems.

Worked Example

Using the default values from the calculator: draft text = “Hello”, max length = 100.

  1. Current length = len(“Hello”) = 5 characters.
  2. Remaining = max(0, 100 − 5) = 95 characters.

The calculator shows Current length = 5 and Remaining = 95, matching the steps above.

Background: Why Immutable State Matters in Blazor Forms

Blazor forms typically bind directly to a mutable model using @bind-Value. When the user types, the model’s properties are updated in place. This is simple, but it creates a problem: the draft is always the same object, so you cannot easily revert to a previous version, compare changes, or know what the user actually edited. Immutable state solves this by treating the draft as a value that is replaced, not modified. Each keystroke produces a new state object, and the old one remains untouched in memory, allowing you to keep a history or implement undo/redo.

How Immutable State Works in Blazor

In Blazor, you typically store the draft in a component field or property. With immutable state, you replace that field with a new instance on every change. Here is a concrete pattern:

  1. Define a record or class with init-only properties (or a constructor that sets them).
  2. In the form, use a local variable that holds the current draft.
  3. On each input event, create a new draft with the updated field, e.g., draft = draft with { Title = newTitle }.
  4. Pass the new draft to the UI, which re-renders with the latest values.

This works because Blazor’s change detection sees a new object reference and re-renders the component. The old draft is still available if you keep a reference to it, which is the basis for undo stacks or audit trails.

Choosing Between Mutable and Immutable State

Not every form needs immutable state. Use it when you need:

  • Undo/redo – you can push each draft onto a stack.
  • Change tracking – you can compare drafts to see what changed.
  • Concurrency safety – multiple components or async operations might read the draft while it is being edited.
  • Stable references – other parts of the app (like a preview pane) can hold a reference to a specific draft version without it mutating underneath them.

For simple forms with no such needs, mutable state is fine and more performant because it avoids allocating new objects on every keystroke. If you are unsure, start with mutable and only switch when you hit a concrete problem.

Common Problems and How to Avoid Them

Immutable state in Blazor forms has a few pitfalls:

  • Over-rendering – every keystroke creates a new object, which can cause the entire component tree to re-render. Use @oninput with a local variable and only update the state after a debounce, or use @key on list items to minimize re-renders.
  • Lost focus – if you replace the entire form object, Blazor may recreate the input elements and lose focus. To avoid this, keep the same component instance and only update the state object, not the component itself. Use @bind:event="oninput" to avoid re-creating the input.
  • Complex nested objects – immutable updates with deep nesting are verbose. Consider using a library like Record or a functional helper, or flatten the state into a single record with many properties.

A common misconception is that immutable state is always slower. In practice, the overhead is negligible for typical form sizes, and the benefits of stability often outweigh the cost.

Comparison: Immutable vs. Mutable State in Blazor

The table below summarizes the key differences:

  • Memory – Immutable creates new objects per change; mutable reuses the same object.
  • Undo – Immutable naturally supports undo by keeping old references; mutable requires manual copying.
  • Performance – Mutable is faster for high-frequency updates; immutable is fine for typical form input.
  • Debugging – Immutable makes state changes explicit, which can simplify debugging.
  • Complexity – Immutable requires more code for nested updates; mutable is straightforward.

In practice, many Blazor developers use a hybrid: mutable for the form fields, but a snapshot of the state for undo and for saving drafts.

Safety and Data Integrity

Immutable state does not automatically make your data safe, but it helps. Because the draft is never mutated, you can always revert to a previous version if a user makes a mistake. This is especially useful for long forms where accidental edits can be costly. However, you still need to handle persistence carefully: when saving, you should send the final state to the server, and you should validate the data before committing. Immutable state does not replace validation or error handling.

Alternatives to Immutable State

If immutable state feels heavy, consider these alternatives:

  • Manual snapshotting – keep a copy of the original form and compare on save.
  • Event sourcing – store a list of changes and reconstruct the state from them.
  • Using a state management library – like Fluxor or Blazor-State, which often use immutable patterns under the hood.
  • Browser storage – save drafts to localStorage on each change, so the draft survives page refresh.

Each has trade-offs, but immutable state is the simplest way to get a stable draft with undo capability without external dependencies.

Frequently Asked Questions

Does immutable state cause Blazor to lose focus in input fields?

It can, if you replace the component itself. But if you keep the same component instance and only update the state object (e.g., via @bind:event="oninput"), focus is preserved. The key is to not re-create the input element by changing its @key or replacing the entire form component.

Can I use immutable state with Blazor’s built-in EditForm?

Yes, you can. EditForm works with any model, but it expects a mutable object for two-way binding. To use immutable state, you can bind to a local property that you update with a new object on each change, and then map the values to the EditForm’s model manually. Alternatively, use a wrapper that implements INotifyPropertyChanged.

Is immutable state worth it for a simple contact form?

Probably not. If you only have a few fields and no undo requirement, mutable state is simpler and faster. Immutable state shines when you need history, change tracking, or concurrent access.

How do I implement undo/redo with immutable state in Blazor?

Maintain a stack of previous states. On each change, push the current state onto the undo stack. To undo, pop the stack and set the current state to that value. For redo, keep a separate stack for undone states. This works because each state is an immutable object that can be reused.

Does immutable state affect performance in Blazor Server?

In Blazor Server, every state change triggers a circuit round-trip. Creating new objects on each keystroke can increase memory usage slightly, but the impact is minimal for typical forms. The larger cost is the network round-trip, which you can mitigate by debouncing input events.

Related

Formula used
in t | Draft text | - | Hello | 0 | 1000
in n | Max length | - | 100 | 1 | 10000
out len | Current length | chars | len(t) | 0
out rem | Remaining | chars | max(0, n - len(t)) | 0

Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →