Server-to-Client State
Pass data from server (MoonBit) to client (TypeScript) with type safety.
The Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Server โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ MoonBit Struct โ
โ โ โ
โ derive(ToJson) โ
โ โ โ
โ .to_json().stringify() โ
โ โ โ
โ HTML: luna:state='{"initial":5}' โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Client โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Luna Loader โ
โ โ โ
โ JSON.parse(luna:state) โ
โ โ โ
โ TypeScript Interface โ
โ โ โ
โ Component props โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Defining State
MoonBit (Server)
using @server_dom { island, button, text }
using @luna { Load }
struct CounterState {
initial : Int
max : Int
} derive(ToJson, FromJson)
fn counter_island(state : CounterState) -> @luna.Node {
island(
id="counter",
url="/static/counter.js",
state=state.to_json().stringify(),
trigger=Load,
children=[
button([text("Count: " + state.initial.to_string())])
],
)
}
TypeScript (Client)
// counter.ts
interface CounterProps {
initial: number;
max: number;
}
function Counter(props: CounterProps) {
const [count, setCount] = createSignal(props.initial);
const increment = () => {
setCount(c => Math.min(c + 1, props.max));
};
return (
<button onClick={increment}>
Count: {count()} / {props.max}
</button>
);
}
hydrate("counter", Counter);
Complex State
Nested Objects
struct UserCardState {
user : User
settings : Settings
} derive(ToJson, FromJson)
struct User {
id : Int
name : String
email : String
} derive(ToJson, FromJson)
struct Settings {
theme : String
compact : Bool
} derive(ToJson, FromJson)
Arrays
struct TodoListState {
todos : Array[Todo]
filter : String
} derive(ToJson, FromJson)
struct Todo {
id : Int
text : String
done : Bool
} derive(ToJson, FromJson)
Enums (Tagged Unions)
enum Status {
Loading
Error(String)
Success(Int)
} derive(ToJson, FromJson)
// Serializes as:
// Loading -> {"$tag": "Loading"}
// Error("msg") -> {"$tag": "Error", "0": "msg"}
// Success(42) -> {"$tag": "Success", "0": 42}
Security Considerations
XSS Prevention
Luna automatically escapes state to prevent XSS:
let state = UserState { name: "<script>alert('xss')</script>" }
// Escaped in HTML attribute
Sensitive Data
Never include sensitive data in state:
// BAD - exposed in HTML source
struct BadState {
api_key : String // Don't do this!
password : String // Never!
} derive(ToJson)
// GOOD - only public data
struct GoodState {
user_id : Int
display_name : String
} derive(ToJson)
State Size
Keep state minimal for performance:
// BAD - too much data
struct BadState {
all_users : Array[User] // Entire database
entire_config : Config // Everything
} derive(ToJson)
// GOOD - only what's needed
struct GoodState {
current_user_id : Int
visible_user_ids : Array[Int] // Just IDs, fetch details client-side
} derive(ToJson)
Type Safety Tips
Keep Types in Sync
Create matching TypeScript types:
// MoonBit
struct CounterState {
initial : Int
max : Int
} derive(ToJson)
// TypeScript
interface CounterState {
initial: number;
max: number;
}
Validate at Runtime (Optional)
import { z } from "zod";
const CounterStateSchema = z.object({
initial: z.number(),
max: z.number(),
});
function Counter(rawProps: unknown) {
const props = CounterStateSchema.parse(rawProps);
// Now props is type-safe
}
Complete Example
MoonBit
struct ProductIslandState {
product : Product
in_cart : Bool
user_currency : String
} derive(ToJson, FromJson)
struct Product {
id : Int
name : String
price : Int
stock : Int
} derive(ToJson, FromJson)
fn product_island(state : ProductIslandState) -> @luna.Node {
island(
id="product",
url="/static/product.js",
state=state.to_json().stringify(),
trigger=Load,
children=[
div([
h2([text(state.product.name)]),
p([text(state.user_currency + " " + state.product.price.to_string())]),
])
],
)
}
TypeScript
interface ProductIslandProps {
product: {
id: number;
name: string;
price: number;
stock: number;
};
inCart: boolean;
userCurrency: string;
}
function ProductIsland(props: ProductIslandProps) {
const [inCart, setInCart] = createSignal(props.inCart);
const [quantity, setQuantity] = createSignal(1);
const addToCart = () => {
setInCart(true);
// API call to add to cart
};
return (
<div>
<h2>{props.product.name}</h2>
<p>{props.userCurrency} {props.product.price}</p>
<Show when={!inCart()} fallback={<p>In Cart!</p>}>
<input
type="number"
value={quantity()}
max={props.product.stock}
/>
<button onClick={addToCart}>Add to Cart</button>
</Show>
</div>
);
}
Try It
Create an island that:
Passes a list of products from server
Includes user's preferred currency
Client can filter and add to cart
Next
Learn about Islands Triggers โ