Skip to content

MinimactComponent API

Base class for all Minimact components. Generated components inherit from this class.

Namespace

csharp
using Minimact.AspNetCore.Core;

Overview

MinimactComponent is the foundation of every Minimact component. When you write TSX and transpile it to C#, the generated class inherits from MinimactComponent.

You rarely write C# that inherits from this directly — instead, you write TSX and let Babel generate the C# for you.

Basic Example

TSX (what you write):

tsx
import { useState } from 'minimact';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Generated C# (what Babel creates):

csharp
public class Counter : MinimactComponent
{
    [UseState]
    private int count = 0;

    protected override VNode Render()
    {
        return new VElement("button",
            new Dictionary<string, string> {
                ["onclick"] = nameof(Increment)
            },
            new VText($"Count: {count}")
        );
    }

    public void Increment()
    {
        count++;
        SetState("count", count);
    }
}

Core Properties

ComponentId

csharp
public string ComponentId { get; protected set; }

Unique identifier for this component instance. Automatically generated as a GUID.

Usage:

csharp
Console.WriteLine($"Component ID: {ComponentId}");
// Output: Component ID: a3f7e8d9-1234-5678-90ab-cdef12345678

ConnectionId

csharp
public string? ConnectionId { get; internal set; }

SignalR connection ID for real-time updates. Set internally by the framework.


Abstract Methods

Render()

csharp
protected abstract VNode Render();

Main render method. Must be implemented by all components. Returns a VNode tree representing the DOM structure.

Generated by Babel from your TSX:

tsx
// Your TSX
<div>
  <h1>{title}</h1>
  <p>{description}</p>
</div>

// Generated Render():
protected override VNode Render()
{
    return new VElement("div", null,
        new VElement("h1", null, new VText(title)),
        new VElement("p", null, new VText(description))
    );
}

State Management

SetState()

csharp
protected void SetState(string key, object value)

Update state and trigger re-render with predictive patching.

Parameters:

  • key — State variable name
  • value — New value

Example:

csharp
SetState("count", count + 1);
SetState("user", new User { Name = "Alice", Age = 30 });

What happens:

  1. Stores previous value for diffing
  2. Updates internal state dictionary
  3. Syncs state back to fields (if using [UseState] attribute)
  4. Triggers render cycle
  5. Rust predictor generates patches
  6. Patches sent to client via SignalR

GetState<T>()

csharp
protected T? GetState<T>(string key)

Get current state value with type safety.

Example:

csharp
var count = GetState<int>("count");
var user = GetState<User>("user");

GetState()

csharp
protected object? GetState(string key)

Get current state value (non-generic).

Example:

csharp
var value = GetState("someKey");

Client State

GetClientState<T>()

csharp
protected T GetClientState<T>(string key, T defaultValue = default!)

Get client-computed value with type safety. Used when the client computes values using external libraries (lodash, moment, etc.) and syncs them to the server.

Example:

tsx
// TSX (client side)
const sortedUsers = useClientState('sortedUsers', () => {
  return _.sortBy(users, 'name'); // Using lodash
});

// C# (server side)
var sortedUsers = GetClientState<List<User>>("sortedUsers", new List<User>());

UpdateClientState()

csharp
public void UpdateClientState(Dictionary<string, object> updates)

Update client-computed state (called by SignalR when client sends computed values). Does NOT trigger a re-render.


Lifecycle Methods

OnInitialized()

csharp
protected virtual void OnInitialized()

Called once when component is first created (synchronous).

Example:

csharp
protected override void OnInitialized()
{
    // Initialize state
    SetState("count", 0);
    SetState("loading", true);
}

OnInitializedAsync()

csharp
public virtual Task OnInitializedAsync()

Called once when component is first created (asynchronous). Calls OnInitialized() first.

Example:

csharp
public override async Task OnInitializedAsync()
{
    // Fetch initial data
    var users = await _db.Users.ToListAsync();
    SetState("users", users);
}

OnStateChanged()

csharp
public virtual void OnStateChanged(string[] changedKeys)

Called after state changes, before patches are sent.

Parameters:

  • changedKeys — Array of state keys that changed

Example:

csharp
public override void OnStateChanged(string[] changedKeys)
{
    if (changedKeys.Contains("userId"))
    {
        // Reload user data when userId changes
        var user = _db.Users.Find(GetState<int>("userId"));
        SetState("user", user);
    }
}

OnRenderError()

csharp
protected virtual VNode? OnRenderError(Exception exception)

Called when an error occurs during rendering (Error Boundary pattern). Override to provide fallback UI instead of crashing.

Parameters:

  • exception — The exception that occurred

Returns:

  • Fallback VNode to display, or null to re-throw

Example:

csharp
protected override VNode? OnRenderError(Exception exception)
{
    // Show error UI instead of crashing
    return new VElement("div",
        new Dictionary<string, string> { ["class"] = "error-boundary" },
        new VText($"Error: {exception.Message}")
    );
}

OnComponentMounted()

csharp
public virtual void OnComponentMounted()

Called after component is first mounted on the client.

OnComponentUnmounted()

csharp
public virtual void OnComponentUnmounted()

Called before component is unmounted from the client. Use for cleanup.


Server Tasks

GetServerTask<T>()

csharp
protected ServerTaskState<T> GetServerTask<T>(string taskId)

Get or create a server task by ID. Used internally by useServerTask hook.

Example (Generated by Babel):

tsx
// Your TSX
const [task, startTask] = useServerTask(async (updateProgress) => {
  for (let i = 0; i <= 100; i += 10) {
    await delay(500);
    updateProgress(i);
  }
  return 'Complete!';
});

// Generated C# method
[ServerTask("task_1")]
private async Task<string> Task_1(IProgress<double> updateProgress, CancellationToken ct)
{
    for (int i = 0; i <= 100; i += 10)
    {
        await Task.Delay(500, ct);
        updateProgress.Report(i);
    }
    return "Complete!";
}

Prediction Hints

RegisterHint()

csharp
protected void RegisterHint(string hintId, Dictionary<string, object> predictedState)

Register a prediction hint for usePredictHint. Pre-computes patches for likely state changes.

Parameters:

  • hintId — Unique identifier for this hint
  • predictedState — Dictionary of predicted state changes

Example (Generated by Babel):

tsx
// Your TSX
usePredictHint(() => ({ count: count + 1 }), 0.95);

// Generated C#
RegisterHint("hint_count_increment", new Dictionary<string, object> {
    ["count"] = count + 1
});

What happens:

  1. Rust predictor generates patches for predicted state
  2. Patches sent to client and cached
  3. When user triggers the state change, cached patch applied instantly (~2-3ms)

Dynamic Bindings

ConfigureDynamicBindings()

csharp
protected virtual void ConfigureDynamicBindings()

Override to register dynamic value bindings. Enables "structure once, bind separately" pattern.

Example:

csharp
protected override void ConfigureDynamicBindings()
{
    DynamicBindings.RegisterBinding(".price", (state) =>
    {
        var isPremium = (bool)state["isPremium"];
        var factoryPrice = (decimal)state["factoryPrice"];
        var retailPrice = (decimal)state["retailPrice"];

        return isPremium ? factoryPrice : retailPrice;
    });
}

TSX stays clean:

tsx
<span className="price"></span>

Logic lives in C#, separate from structure.


Utility Methods

ForceUpdate()

csharp
protected void ForceUpdate()

Force a re-render without state change.

Example:

csharp
public void RefreshUI()
{
    // Force re-render even if state didn't change
    ForceUpdate();
}

GetMetadata()

csharp
public ComponentMetadata GetMetadata()

Get component metadata for Rust predictor. Extracts loop templates from attributes.

Returns:

  • ComponentMetadata with loop templates

Used internally by predictive rendering system.


State Synchronization (Client → Server)

These methods are called by SignalR to keep server state in sync with client. You don't call these directly.

SetStateFromClient()

csharp
public void SetStateFromClient(string key, object value)

Set state from client-side useState hook. Keeps server in sync to prevent stale data.

SetDomStateFromClient()

csharp
public void SetDomStateFromClient(string key, DomElementStateSnapshot snapshot)

Set DOM element state from client-side useDomElementState hook.

SetQueryResultsFromClient()

csharp
public void SetQueryResultsFromClient(string key, List<Dictionary<string, object>> results)

Set query results from client-side useDomQuery hook.

TransitionLifecycleFromClient()

csharp
public bool TransitionLifecycleFromClient(string newState)

Transition lifecycle state from client. Returns true if transition was valid.


Advanced: Lifecycle State Machine

InitializeLifecycle()

csharp
protected void InitializeLifecycle(LifecycleStateConfig config)

Initialize a lifecycle state machine for this component. Call in constructor.

Example:

csharp
public MyComponent()
{
    InitializeLifecycle(new LifecycleStateConfig
    {
        InitialState = "idle",
        States = new Dictionary<string, List<string>>
        {
            ["idle"] = new List<string> { "loading" },
            ["loading"] = new List<string> { "success", "error" },
            ["success"] = new List<string> { "idle" },
            ["error"] = new List<string> { "idle" }
        }
    });
}

Complete Example

TSX:

tsx
import { useState, useServerTask } from 'minimact';

export function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);

  const [task, loadUsers] = useServerTask(async (updateProgress) => {
    const allUsers = await db.Users.ToListAsync();
    for (let i = 0; i < allUsers.length; i++) {
      updateProgress((i / allUsers.length) * 100);
      await Task.Delay(100);
    }
    return allUsers;
  });

  useEffect(() => {
    if (task.isComplete) {
      setUsers(task.result);
    }
  }, [task.isComplete]);

  return (
    <div>
      <button onClick={loadUsers} disabled={task.isRunning}>
        Load Users
      </button>

      {task.isRunning && <p>Loading: {task.progress}%</p>}

      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Generated C# (simplified):

csharp
public class UserList : MinimactComponent
{
    [UseState]
    private List<User> users = new();

    [UseState]
    private bool loading = false;

    [ServerTask("loadUsers")]
    private async Task<List<User>> LoadUsersTask(
        IProgress<double> updateProgress,
        CancellationToken ct)
    {
        var allUsers = await _db.Users.ToListAsync(ct);
        for (int i = 0; i < allUsers.Count; i++)
        {
            updateProgress.Report((i / (double)allUsers.Count) * 100);
            await Task.Delay(100, ct);
        }
        return allUsers;
    }

    public override async Task OnInitializedAsync()
    {
        // Initialization logic
    }

    protected override VNode Render()
    {
        var taskState = GetServerTask<List<User>>("loadUsers");

        return new VElement("div", null,
            // Button to start task
            new VElement("button",
                new Dictionary<string, string> {
                    ["onclick"] = nameof(StartLoadUsers),
                    ["disabled"] = taskState.IsRunning.ToString().ToLower()
                },
                new VText("Load Users")
            ),

            // Progress indicator
            taskState.IsRunning ?
                new VElement("p", null,
                    new VText($"Loading: {taskState.Progress}%")
                ) : null,

            // User list
            new VElement("ul", null,
                users.Select(user =>
                    new VElement("li",
                        new Dictionary<string, string> { ["key"] = user.Id.ToString() },
                        new VText(user.Name)
                    )
                ).ToArray()
            )
        );
    }

    public void StartLoadUsers()
    {
        var task = GetServerTask<List<User>>("loadUsers");
        _ = task.StartAsync();
    }
}

See Also


Next: StateManager API →

Released under the MIT License.