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/core';

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)
  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/core';

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.