Why Minimact Doesn't Need 'use client'
One of the most profound architectural decisions in Minimact is the complete absence of 'use client' directives — and it's not a limitation, it's a superpower.
🍵 The Minimact Tea Party: No Taxation Without Justification
"No hydration without justification!"
Just as the Boston Tea Party was a revolt against oppressive taxation, Minimact is a rebellion against React's tax regime:
React's Three Taxes
- Boundary Tax - You must declare
'use client'and manually split your components - Hydration Tax - You must ship 50KB+ and wait 200ms for the client to re-execute everything
- Bundle Tax - Every interactive component increases your JavaScript payload
Minimact's Declaration of Independence
We threw that tea into the harbor. ☕➡️🌊
Minimact rejected these taxes entirely:
- ✅ No boundary tax - No
'use client'directives, no mental overhead - ✅ No hydration tax - DOM is pre-rendered and ready, zero "wake up" time
- ✅ No bundle tax - ~10KB runtime regardless of component count
This is the Posthydrationist Manifesto in action - we've moved beyond the hydration era entirely.
The Problem with 'use client'
In React Server Components (RSC), you must manually declare which components run on the client:
'use client'; // ← Manual boundary declaration
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}This creates several problems:
1. Boundary Tax
You must decide upfront where the server/client split happens. This decision is:
- Contagious:
'use client'spreads up the component tree - Irreversible: Once a component is client-side, it can't access server-only APIs
- Manual: You must analyze every component to determine its placement
2. Bundle Bloat
When you mark a component as 'use client', you ship:
- The entire component code
- All its dependencies
- React's client runtime
- State management logic
A simple counter can balloon to 50KB+ of JavaScript.
3. Split Component Anti-Pattern
RSC forces you to artificially split components:
// ❌ Can't do this - server component can't have state
function ServerProduct({ product }) {
const [quantity, setQuantity] = useState(1); // ERROR!
return <div>...</div>;
}
// ❌ Can't do this - client component can't use server data
'use client';
function ClientProduct() {
const product = await db.products.get(id); // ERROR!
return <div>...</div>;
}
// ✅ Have to split into TWO components
function ServerProduct({ product }) {
return <ClientQuantitySelector initialProduct={product} />;
}
'use client';
function ClientQuantitySelector({ initialProduct }) {
const [quantity, setQuantity] = useState(1);
return <div>...</div>;
}This is artificial complexity imposed by the framework.
Minimact's Solution: Static Analysis Over Runtime Annotations
Minimact eliminates 'use client' entirely through build-time intelligence.
How It Works
The Babel plugin analyzes your JSX at build time and automatically determines:
- What's static → Rendered once as HTML
- What's dynamic → Needs template patches
- What's interactive → Event handlers captured via closures
public partial class Counter : MinimactComponent
{
[State] private int count = 0;
protected override VNode Render() {
return <button onClick={() => count++}>{count}</button>;
}
}The transpiler sees:
{count}→ Dynamic text binding, generate template patchonClick={() => count++}→ Server-side handler, capture via SignalR<button>→ Static HTML structure
No manual annotation needed.
The Four Pillars of 'use client'-Free Architecture
1. Zero Client-Side JavaScript for Logic
React RSC:
'use client';
function ProductCard({ product }) {
const [quantity, setQuantity] = useState(1);
const total = product.price * quantity; // ← Runs on client
return (
<div>
<input
value={quantity}
onChange={e => setQuantity(+e.target.value)}
/>
<p>Total: ${total.toFixed(2)}</p>
</div>
);
}Shipped to client: All logic + React runtime ≈ 50KB+
Minimact:
[State] private int quantity = 1;
protected override VNode Render() {
var total = Product.Price * quantity; // ← Server calculation
return (
<div>
<input
value={quantity}
onInput={(e) => quantity = int.Parse(e.target.value)}
/>
<p>Total: ${total.ToString("F2")}</p>
</div>
);
}Shipped to client: Minimact runtime ≈ ~10KB + patch metadata
2. Automatic Granular Reactivity
The template patch system means updates are surgical, not wholesale:
<div>
<h1>{user.name}</h1>
<p>Score: {user.score + bonus}</p>
</div>When user.score changes:
React RSC:
- Re-render component tree
- Diff virtual DOM
- Patch real DOM
Minimact:
- Server: Calculate new value →
95 - Rust: Generate patch for only
p.text[1] - Client: Apply single text node update
No client-side re-rendering. No reconciliation. Just direct patches.
3. The Closure Capture System
Event handlers with loop variables "just work":
{todos.map(todo => (
<button onClick={() => DeleteTodo(todo.id)}>Delete</button>
))}Transpiled to:
todos.Select(todo => new VElement("button", new {
onClick = $"Handle0|{{\"id\":{todo.id}}}" // ← Closure captured!
}, "Delete"))Client receives:
<button data-handler="Handle0|{"id":42}">Delete</button>On click:
- Client parses:
Handle0with{id: 42} - SignalR → Server:
InvokeHandler("Handle0", {id: 42}) - Server executes
DeleteTodo(42)with full C# context - Server re-renders → Rust diffs → Client patches
Zero manual serialization. Zero 'use client'. Just works.
4. Predictive Rendering: Client Speed, Server Logic
With parameterized templates, the client can apply patches instantly:
[State] private bool isOpen = false;
<button onClick={() => isOpen = !isOpen}>
{isOpen ? "Close" : "Open"}
</button>Build-time template generation:
{
"template": "{0}",
"bindings": ["isOpen"],
"conditionalTemplates": {
"true": "Close",
"false": "Open"
}
}Runtime flow:
- Click → Client instantly applies cached patch (0ms)
- Client sends state change to server via SignalR
- Server confirms (or corrects if needed)
This is faster than React's optimistic updates because:
- ✅ No client-side vDOM diffing
- ✅ No reconciliation algorithm
- ✅ Direct DOM patch from pre-computed template
Composition Without Compromise
The killer feature: unified component model.
React forces artificial splits:
// Server component
async function ProductPage({ id }) {
const product = await db.products.get(id);
return <ClientInteractive product={product} />;
}
// Client component (separate file)
'use client';
function ClientInteractive({ product }) {
const [quantity, setQuantity] = useState(1);
return <div>{/* Can't access db here! */}</div>;
}Minimact keeps it unified:
public partial class ProductPage : MinimactComponent
{
[Inject] private IProductService _products;
[State] private int quantity = 1;
protected override VNode Render() {
// ✅ Server-side data access
var product = _products.Get(ProductId);
// ✅ Client-side interactivity
var total = product.Price * quantity;
return (
<div>
<h1>{product.Name}</h1>
<input
value={quantity}
onInput={(e) => quantity = int.Parse(e.target.value)}
/>
<p>Total: ${total.ToString("F2")}</p>
</div>
);
}
}No splitting. No boundaries. No 'use client'. Just component logic.
Technical Deep Dive: How Minimact Achieves This
Build-Time Analysis
The Babel plugin performs AST analysis on your JSX:
<div>
<h1>{user.name}</h1>
<button onClick={() => Save(user.id)}>Save</button>
<p>Count: {items.length}</p>
</div>Extracted metadata:
{
"staticStructure": "<div><h1></h1><button>Save</button><p>Count: </p></div>",
"dynamicBindings": {
"h1.text[0]": { "binding": "user.name" },
"p.text[1]": { "binding": "items.length" }
},
"eventHandlers": {
"button.onClick": {
"handler": "Handle0",
"closure": { "userId": "user.id" }
}
}
}This metadata enables:
- Server to render initial HTML
- Client to know what to patch
- Runtime to route events to server handlers
Runtime Coordination
┌─────────────┐ ┌─────────────┐
│ Browser │ │ Server │
│ │ │ │
│ 1. Click │ │ │
│ ─────────> │ │ │
│ │ 2. SignalR Call │ │
│ │ ──────────────────>│ 3. Execute │
│ │ │ Handler │
│ │ │ │
│ │ 4. Render & Diff │ │
│ │ <───────────────│ │
│ │ │ │
│ 5. Patch │ 6. Send Patches │ │
│ DOM <───│────────────────────│ │
└─────────────┘ └─────────────┘Key insight: The client is a dumb terminal that:
- Displays HTML
- Captures events
- Applies patches
All logic stays on the server where it belongs.
Comparison Table
| Feature | React RSC | Minimact |
|---|---|---|
| Boundary Declaration | Manual 'use client' | Automatic (build-time) |
| Component Splitting | Required | Not needed |
| Client Bundle Size | 50KB+ per component | ~10KB total runtime |
| Client-Side Logic | Yes (useState, effects) | No (patches only) |
| Server Data Access | Server components only | All components |
| Event Handling | Client-side callbacks | Server-side with closure capture |
| State Updates | Client re-render | Server render → Rust diff → Client patch |
| Predictive Updates | Optimistic UI patterns | Built-in template patches |
| Type Safety | TypeScript (client) | C# (server) |
Real-World Impact
Example: Todo App
React RSC approach:
// server component
async function TodoList() {
const todos = await db.todos.getAll();
return <ClientTodoList todos={todos} />;
}
// client component (separate file)
'use client';
function ClientTodoList({ todos: initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
async function toggleTodo(id) {
await fetch('/api/todos/' + id, { method: 'PATCH' });
setTodos(todos.map(t => t.id === id ? {...t, done: !t.done} : t));
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}Bundle size: ~45KB (React + component + state management)
Minimact approach:
public partial class TodoList : MinimactComponent
{
[Inject] private ITodoService _todos;
protected override VNode Render() {
var todos = _todos.GetAll();
return (
<ul>
{todos.Select(todo => (
<li key={todo.Id}>
<input
type="checkbox"
checked={todo.Done}
onChange={() => _todos.Toggle(todo.Id)}
/>
{todo.Text}
</li>
)).ToArray()}
</ul>
);
}
}Bundle size: ~10KB (Minimact runtime only) Bonus: Direct database access, type safety, no API layer needed
The Philosophical Shift
React Server Components ask: "Where should this component run?"
Minimact answers: "Components run on the server. The client is just a view."
This isn't a limitation — it's architecture as advantage.
You get:
- ✅ Server-side data access
- ✅ Client-side interactivity feel
- ✅ Type safety (C#)
- ✅ Zero client-side logic bundles
- ✅ Instant UI updates (predictive rendering)
- ✅ No mental tax of boundary management
- ✅ Unified component model
Conclusion
'use client' exists because React chose to run components in both places and needed a way to distinguish them.
Minimact chose to run components in ONE place (server) and uses:
- Smart templates (build-time analysis)
- Surgical patches (Rust diffing)
- Event proxying (SignalR handlers)
- Predictive rendering (cached templates)
To make the client feel instant while keeping all logic server-side.
This isn't just competitive with React — it's a different league entirely.
No 'use client' needed. Ever. 🚀
🐍 Don't Tread on My Bundle Size
"No taxation without representation. No hydration without justification."
— The Minimact Manifesto
See Also
- Posthydrationist Manifesto - The philosophy behind this approach
- Template Patch System - How templates enable client-side speed
- State Synchronization - Keeping client and server in sync
- Predictive Rendering 101 - Instant updates without 'use client'
