チュートリアル: JavaScript

サーバーからクライアントへの State

型安全性を持ってサーバーからクライアントにデータを渡します。

問題

サーバーデータをクライアント JavaScript にどうやって渡しますか?

サーバー(MoonBit)            クライアント(TypeScript)
     │                              │
     │  データ付き HTML をレンダリング │
     │ ──────────────────────────>  │
     │                              │
     │      props をどう渡す?       │
     └──────────────────────────────┘

解決策

Luna は状態を JSON として HTML にシリアライズ:

<div
  luna:id="counter"
  luna:state='{"initial":5,"max":100}'
  luna:url="/static/counter.js"
>
  <!-- SSR コンテンツ -->
</div>

State の定義

サーバーサイド

サーバーは luna:state 属性で状態を JSON としてシリアライズします。MoonBit でのサーバーサイドレンダリングについては、MoonBit チュートリアルを参照してください。

クライアントサイド(TypeScript)

// counter.ts
import { createSignal, hydrate } from '@luna_ui/luna';

// マッチする TypeScript インターフェースを定義
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);

複雑な State

ネストしたオブジェクト

interface UserCardProps {
  user: {
    id: number;
    name: string;
    email: string;
  };
  settings: {
    theme: string;
    compact: boolean;
  };
}

配列

interface TodoListProps {
  todos: Array<{
    id: number;
    text: string;
    done: boolean;
  }>;
  filter: string;
}

型安全性のヒント

1. 型を同期させる

共有型定義を作成するか生成:

// types.ts - MoonBit 構造体と同期を維持
export interface CounterProps {
  initial: number;
  max: number;
}

2. 欠損/デフォルト値の処理

interface Props {
  count?: number;
  label?: string;
}

function Counter(props: Props) {
  const count = props.count ?? 0;
  const label = props.label ?? "Count";

  // ...
}

セキュリティ考慮事項

機密データ

状態に機密データを含めないこと:

// 悪い例 - HTML ソースに公開される
interface BadProps {
  apiKey: string;      // やめて!
  password: string;    // 絶対だめ!
}

// 良い例 - 公開データのみ
interface GoodProps {
  userId: number;
  displayName: string;
}

試してみよう

商品ページ Island の状態構造を設計:

解答
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 呼び出し
  };

  return (
    <div>
      <h2>{props.product.name}</h2>
      <p>{props.userCurrency} {props.product.price}</p>
      <p>在庫: {props.product.stock}</p>

      <Show when={!inCart()} fallback={<p>カートに入っています!</p>}>
        <input
          type="number"
          value={quantity()}
          onChange={(e) => setQuantity(+e.target.value)}
          max={props.product.stock}
        />
        <button onClick={addToCart}>カートに追加</button>
      </Show>
    </div>
  );
}

次へ

Web Components Islands → について学ぶ