MinimactComponent API
Base class for all Minimact components. Generated components inherit from this class.
Namespace
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):
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):
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
public string ComponentId { get; protected set; }Unique identifier for this component instance. Automatically generated as a GUID.
Usage:
Console.WriteLine($"Component ID: {ComponentId}");
// Output: Component ID: a3f7e8d9-1234-5678-90ab-cdef12345678ConnectionId
public string? ConnectionId { get; internal set; }SignalR connection ID for real-time updates. Set internally by the framework.
Abstract Methods
Render()
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:
// 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()
protected void SetState(string key, object value)Update state and trigger re-render with predictive patching.
Parameters:
key— State variable namevalue— New value
Example:
SetState("count", count + 1);
SetState("user", new User { Name = "Alice", Age = 30 });What happens:
- Stores previous value for diffing
- Updates internal state dictionary
- Syncs state back to fields (if using
[UseState]attribute) - Triggers render cycle
- Rust predictor generates patches
- Patches sent to client via SignalR
GetState<T>()
protected T? GetState<T>(string key)Get current state value with type safety.
Example:
var count = GetState<int>("count");
var user = GetState<User>("user");GetState()
protected object? GetState(string key)Get current state value (non-generic).
Example:
var value = GetState("someKey");Client State
GetClientState<T>()
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 (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()
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()
protected virtual void OnInitialized()Called once when component is first created (synchronous).
Example:
protected override void OnInitialized()
{
// Initialize state
SetState("count", 0);
SetState("loading", true);
}OnInitializedAsync()
public virtual Task OnInitializedAsync()Called once when component is first created (asynchronous). Calls OnInitialized() first.
Example:
public override async Task OnInitializedAsync()
{
// Fetch initial data
var users = await _db.Users.ToListAsync();
SetState("users", users);
}OnStateChanged()
public virtual void OnStateChanged(string[] changedKeys)Called after state changes, before patches are sent.
Parameters:
changedKeys— Array of state keys that changed
Example:
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()
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
VNodeto display, ornullto re-throw
Example:
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()
public virtual void OnComponentMounted()Called after component is first mounted on the client.
OnComponentUnmounted()
public virtual void OnComponentUnmounted()Called before component is unmounted from the client. Use for cleanup.
Server Tasks
GetServerTask<T>()
protected ServerTaskState<T> GetServerTask<T>(string taskId)Get or create a server task by ID. Used internally by useServerTask hook.
Example (Generated by Babel):
// 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()
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 hintpredictedState— Dictionary of predicted state changes
Example (Generated by Babel):
// Your TSX
usePredictHint(() => ({ count: count + 1 }), 0.95);
// Generated C#
RegisterHint("hint_count_increment", new Dictionary<string, object> {
["count"] = count + 1
});What happens:
- Rust predictor generates patches for predicted state
- Patches sent to client and cached
- When user triggers the state change, cached patch applied instantly (~2-3ms)
Dynamic Bindings
ConfigureDynamicBindings()
protected virtual void ConfigureDynamicBindings()Override to register dynamic value bindings. Enables "structure once, bind separately" pattern.
Example:
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:
<span className="price"></span>Logic lives in C#, separate from structure.
Utility Methods
ForceUpdate()
protected void ForceUpdate()Force a re-render without state change.
Example:
public void RefreshUI()
{
// Force re-render even if state didn't change
ForceUpdate();
}GetMetadata()
public ComponentMetadata GetMetadata()Get component metadata for Rust predictor. Extracts loop templates from attributes.
Returns:
ComponentMetadatawith 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()
public void SetStateFromClient(string key, object value)Set state from client-side useState hook. Keeps server in sync to prevent stale data.
SetDomStateFromClient()
public void SetDomStateFromClient(string key, DomElementStateSnapshot snapshot)Set DOM element state from client-side useDomElementState hook.
SetQueryResultsFromClient()
public void SetQueryResultsFromClient(string key, List<Dictionary<string, object>> results)Set query results from client-side useDomQuery hook.
TransitionLifecycleFromClient()
public bool TransitionLifecycleFromClient(string newState)Transition lifecycle state from client. Returns true if transition was valid.
Advanced: Lifecycle State Machine
InitializeLifecycle()
protected void InitializeLifecycle(LifecycleStateConfig config)Initialize a lifecycle state machine for this component. Call in constructor.
Example:
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:
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):
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
- StateManager API — State management internals
- ComponentRegistry API — Component lifecycle
- MinimactHub API — SignalR hub
- VNode API — Virtual DOM nodes
- Client Hooks — TypeScript/React hooks
Next: StateManager API →
