Islands: State

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 โ†’