Server-to-Client State
Pass data from server to client with type safety.
The Problem
How do you pass server data to client JavaScript?
Server (MoonBit) Client (TypeScript)
โ โ
โ Render HTML with data โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโ> โ
โ โ
โ How to pass props? โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Solution
Luna serializes state as JSON in the HTML:
<div
luna:id="counter"
luna:state='{"initial":5,"max":100}'
luna:url="/static/counter.js"
>
<!-- SSR content -->
</div>
Defining State
Server Side
The server serializes state as JSON in the luna:state attribute. For server-side rendering with MoonBit, see the MoonBit Tutorial.
Client Side (TypeScript)
// counter.ts
import { createSignal, hydrate } from '@luna_ui/luna';
// Define matching TypeScript interface
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
interface UserCardProps {
user: {
id: number;
name: string;
email: string;
};
settings: {
theme: string;
compact: boolean;
};
}
Arrays
interface TodoListProps {
todos: Array<{
id: number;
text: string;
done: boolean;
}>;
filter: string;
}
State 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 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Type Safety Tips
1. Keep Types in Sync
Create shared type definitions or generate them:
// types.ts - Keep in sync with MoonBit structs
export interface CounterProps {
initial: number;
max: number;
}
export interface UserProps {
id: number;
name: string;
role: "admin" | "user" | "guest";
}
2. Validate at Runtime
For extra safety, validate incoming props:
import { z } from "zod";
const CounterPropsSchema = z.object({
initial: z.number(),
max: z.number(),
});
function Counter(rawProps: unknown) {
const props = CounterPropsSchema.parse(rawProps);
// Now props is type-safe
}
3. Handle Missing/Default Values
interface Props {
count?: number;
label?: string;
}
function Counter(props: Props) {
const count = props.count ?? 0;
const label = props.label ?? "Count";
// ...
}
Security Considerations
XSS Prevention
Luna automatically escapes state to prevent XSS. The luna:state attribute is safely encoded.
Sensitive Data
Never include sensitive data in state:
// BAD - exposed in HTML source
interface BadProps {
apiKey: string; // Don't do this!
password: string; // Never!
}
// GOOD - only public data
interface GoodProps {
userId: number;
displayName: string;
}
State Size
Keep state minimal for performance:
// BAD - too much data
interface BadProps {
allUsers: User[]; // Entire database
entireConfig: Config; // Everything
}
// GOOD - only what's needed
interface GoodProps {
currentUserId: number;
visibleUserIds: number[]; // Just IDs, fetch details client-side
}
Try It
Design the state structure for a product page island:
Solution
interface ProductIslandProps {
product: {
id: number;
name: string;
price: number;
stock: number;
imageUrl: string;
};
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>
<p>In stock: {props.product.stock}</p>
<Show when={!inCart()} fallback={<p>In Cart!</p>}>
<input
type="number"
value={quantity()}
onChange={(e) => setQuantity(+e.target.value)}
max={props.product.stock}
/>
<button onClick={addToCart}>Add to Cart</button>
</Show>
</div>
);
}
Next
Learn about Web Components Islands โ