$propsルーン

$propsは、Svelte 5のRunesシステムでコンポーネント間のデータ受け渡しを行うための機能です。このページでは、$propsの基本的な使い方から、TypeScriptとの統合、高度なパターンまで、実践的な活用方法を解説します。

React/Vue経験者向け
  • $propsは React の props や Vue の props に相当
  • TypeScriptの型定義により完全な型安全性を提供
  • デフォルト値の設定やrest propsのサポート
  • Svelte 5では従来のexport letから$propsに移行

$propsとは

$propsは、親コンポーネントからプロパティを受け取るためのルーンです。Svelte 5では、従来のexport letの代わりに$propsを使用します。

主な特徴

  • 型安全: TypeScriptとの完全な統合
  • デフォルト値: 分割代入時にデフォルト値を設定可能
  • Rest Props: 残りのプロパティを...restで受け取り可能
  • リアクティブ: propsの変更は自動的に追跡される
重要な変更点

Svelte 5ではexport letは非推奨となり、$propsの使用が推奨されます:

// ❌ 古い書き方(Svelte 4以前)
export let name: string;
export let age = 0;

// ✅ 新しい書き方(Svelte 5)
let { name, age = 0 } = $props<{ name: string; age?: number }>();
typescript

基本的な使い方

シンプルなProps

// Button.svelte
type Props = {
  label: string;
  disabled?: boolean;
};

let { label, disabled = false }: Props = $props();
typescript
<!-- 親コンポーネント -->
<Button label="クリック" disabled={true} />
svelte

デフォルト値

type Props = {
  count?: number;
  message?: string;
  items?: string[];
};

let { 
  count = 0,
  message = 'デフォルトメッセージ',
  items = []
}: Props = $props();
typescript

Rest Props

残りのプロパティをまとめて受け取ることができます。

type Props = {
  variant: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
};

let { 
  variant,
  size = 'medium',
  ...restProps
}: Props = $props();

// restPropsには、variant と size 以外のすべてのプロパティが含まれる
typescript
<!-- ボタン要素に残りのプロパティを展開 -->
<button {...restProps} class="btn btn-{variant} btn-{size}">
  <slot />
</button>
svelte

高度な型定義

HTMLAttributes との組み合わせ

import type { HTMLButtonAttributes } from 'svelte/elements';

type Props = HTMLButtonAttributes & {
  variant?: 'primary' | 'secondary' | 'danger';
  loading?: boolean;
};

let { 
  variant = 'primary',
  loading = false,
  disabled,
  ...restProps
}: Props = $props();

// disabled は loading 中も true にする
$: isDisabled = disabled || loading;
typescript

ジェネリック型を使用

// SelectList.svelte
type Props<T> = {
  items: T[];
  selected?: T;
  getLabel?: (item: T) => string;
  getValue?: (item: T) => string;
};

let { 
  items,
  selected,
  getLabel = (item) => String(item),
  getValue = (item) => String(item)
}: Props<T> = $props();
typescript

イベントハンドラ

type Props = {
  onClick?: (event: MouseEvent) => void;
  onChange?: (value: string) => void;
  onSubmit?: (data: FormData) => void;
};

let { onClick, onChange, onSubmit }: Props = $props();

function handleClick(event: MouseEvent) {
  // 内部処理
  console.log('ボタンクリック');
  
  // 親のハンドラを呼び出す
  onClick?.(event);
}
typescript

子要素としてのコンポーネント

import type { Snippet } from 'svelte';

type Props = {
  title: string;
  icon?: Snippet;
  actions?: Snippet;
  children: Snippet;
};

let { title, icon, actions, children }: Props = $props();
typescript
<div class="card">
  <header>
    {#if icon}
      {@render icon()}
    {/if}
    <h2>{title}</h2>
    {#if actions}
      {@render actions()}
    {/if}
  </header>
  <main>
    {@render children()}
  </main>
</div>
svelte

実践例:コンポーネントライブラリ

$propsを活用した再利用可能なコンポーネントライブラリの実装例です。ボタン、カード、フォーム要素など、実際のプロジェクトで使えるコンポーネントを作成します。

🎨 $propsを使ったコンポーネントライブラリ

Buttonコンポーネント

Cardコンポーネント

基本カード

シンプルなカード

これは基本的なカードコンポーネントです。$propsを使って親から必要なデータを受け取っています。
📷

画像付きカード

ビジュアル重視

プレースホルダー画像を使用したカードです。実際のプロジェクトでは画像URLを渡します。

アクション付き

インタラクティブ

フッターにアクションボタンがあります。Snippetを使って柔軟なレイアウトを実現しています。

Alertコンポーネント

情報: これは情報アラートです。重要な情報をユーザーに伝えます。
警告: この操作には注意が必要です。dismissibleプロパティで閉じることができます。
エラー: 問題が発生しました。エラーの詳細を確認してください。

FormFieldコンポーネント

連絡先のメールアドレスを入力してください
任意のメッセージを入力できます

コンポーネントのソースコード

以下は、上記デモで使用している各コンポーネントの実装です:

PropsDemo(親コンポーネント)

<!-- PropsDemo.svelte -->
<script lang="ts">
  import Button from './Button.svelte';
  import Card from './Card.svelte';
  import Alert from './Alert.svelte';
  import SimpleFormField from './SimpleFormField.svelte';
  
  // デモ用の状態
  let formData = $state({
    username: '',
    email: '',
    message: ''
  });
  
  let errors = $state<Record<string, string>>({});
  let showSuccessAlert = $state(false);
  
  function handleButtonClick(variant: string, size: string) {
    alert(`${variant} ${size} ボタンがクリックされました!`);
  }
  
  function validateForm() {
    errors = {};
    
    if (!formData.username) {
      errors.username = 'ユーザー名は必須です';
    }
    if (!formData.email) {
      errors.email = 'メールアドレスは必須です';
    } else if (!formData.email.includes('@')) {
      errors.email = '有効なメールアドレスを入力してください';
    }
    
    return Object.keys(errors).length === 0;
  }
  
  function handleSubmit() {
    if (validateForm()) {
      showSuccessAlert = true;
      setTimeout(() => {
        showSuccessAlert = false;
      }, 3000);
      formData = { username: '', email: '', message: '' };
    }
  }
</script>

<div class="demo-container">
  <h2>🎨 $propsを使ったコンポーネントライブラリ</h2>
  
  <!-- ボタンコンポーネント -->
  <section class="component-section">
    <h3>Buttonコンポーネント</h3>
    <div class="button-grid">
      <Button onClick={() => handleButtonClick('primary', 'small')} size="small">
        Small Primary
      </Button>
      <Button onClick={() => handleButtonClick('primary', 'medium')}>
        Medium Primary
      </Button>
      <!-- 他のボタンも同様に配置... -->
    </div>
  </section>
  
  <!-- カードコンポーネント -->
  <section class="component-section">
    <h3>Cardコンポーネント</h3>
    <div class="card-grid">
      <Card title="基本カード" subtitle="シンプルなカード">
        これは基本的なカードコンポーネントです。
      </Card>
      
      <Card title="アクション付き" subtitle="インタラクティブ">
        {#snippet footer()}
          <Button size="small">詳細</Button>
          <Button variant="secondary" size="small">共有</Button>
        {/snippet}
        フッターにアクションボタンがあります。
      </Card>
    </div>
  </section>
  
  <!-- フォームコンポーネント -->
  <section class="component-section">
    <h3>FormFieldコンポーネント</h3>
    <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <SimpleFormField
        label="ユーザー名"
        required
        bind:value={formData.username}
        error={errors.username}
        placeholder="ユーザー名を入力"
      />
      
      <SimpleFormField
        label="メールアドレス"
        type="email"
        required
        bind:value={formData.email}
        error={errors.email}
        helpText={!errors.email ? "連絡先のメールアドレスを入力してください" : undefined}
        placeholder="email@example.com"
      />
      
      <div class="form-actions">
        <Button onClick={handleSubmit}>送信</Button>
        <Button variant="secondary" onClick={() => {...}}>リセット</Button>
      </div>
    </form>
  </section>
</div>
svelte

Buttonコンポーネント

<!-- Button.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  
  type Props = {
    variant?: 'primary' | 'secondary' | 'danger';
    size?: 'small' | 'medium' | 'large';
    disabled?: boolean;
    onClick?: () => void;
    children: Snippet;
  };
  
  let { 
    variant = 'primary', 
    size = 'medium', 
    disabled = false,
    onClick,
    children 
  }: Props = $props();
</script>

<button
  class="btn btn-{variant} btn-{size}"
  {disabled}
  onclick={onClick}
>
  {@render children()}
</button>

<style>
  .btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }
  
  .btn-primary {
    background: #ff3e00;
    color: white;
  }
  
  .btn-primary:hover:not(:disabled) {
    background: #ff5a00;
  }
  
  .btn-secondary {
    background: #6c757d;
    color: white;
  }
  
  .btn-secondary:hover:not(:disabled) {
    background: #5a6268;
  }
  
  .btn-danger {
    background: #dc3545;
    color: white;
  }
  
  .btn-danger:hover:not(:disabled) {
    background: #c82333;
  }
  
  .btn-small {
    padding: 0.25rem 0.5rem;
    font-size: 0.875rem;
  }
  
  .btn-medium {
    padding: 0.5rem 1rem;
    font-size: 1rem;
  }
  
  .btn-large {
    padding: 0.75rem 1.5rem;
    font-size: 1.125rem;
  }
  
  .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
</style>
svelte

Cardコンポーネント

<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  
  type Props = {
    title: string;
    subtitle?: string;
    image?: string;
    footer?: Snippet;
    children: Snippet;
  };
  
  let { 
    title,
    subtitle,
    image,
    footer,
    children 
  }: Props = $props();
</script>

<div class="card">
  {#if image}
    <div class="card-image">
      {#if image === 'placeholder'}
        <div class="placeholder">📷</div>
      {:else}
        <img src={image} alt={title} />
      {/if}
    </div>
  {/if}
  
  <div class="card-header">
    <h4>{title}</h4>
    {#if subtitle}
      <p class="card-subtitle">{subtitle}</p>
    {/if}
  </div>
  
  <div class="card-body">
    {@render children()}
  </div>
  
  {#if footer}
    <div class="card-footer">
      {@render footer()}
    </div>
  {/if}
</div>

<style>
  .card {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    overflow: hidden;
  }
  
  .card-image {
    height: 150px;
    background: #f0f0f0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .card-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  
  .placeholder {
    font-size: 3rem;
  }
  
  .card-header {
    padding: 1rem;
    border-bottom: 1px solid #eee;
  }
  
  .card-header h4 {
    margin: 0;
    color: #333;
  }
  
  .card-subtitle {
    margin: 0.25rem 0 0;
    color: #666;
    font-size: 0.875rem;
  }
  
  .card-body {
    padding: 1rem;
  }
  
  .card-footer {
    padding: 1rem;
    border-top: 1px solid #eee;
    display: flex;
    gap: 0.5rem;
  }
</style>
svelte

Alertコンポーネント

<!-- Alert.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  
  type Props = {
    type?: 'info' | 'success' | 'warning' | 'error';
    title?: string;
    dismissible?: boolean;
    onDismiss?: () => void;
    children: Snippet;
  };
  
  let { 
    type = 'info',
    title,
    dismissible = false,
    onDismiss,
    children 
  }: Props = $props();
  
  let visible = $state(true);
  
  function handleDismiss() {
    visible = false;
    onDismiss?.();
  }
</script>

{#if visible}
  <div class="alert alert-{type}">
    <div class="alert-content">
      {#if title}
        <strong>{title}:</strong>
      {/if}
      {@render children()}
    </div>
    {#if dismissible}
      <button class="alert-close" onclick={handleDismiss}>
        ×
      </button>
    {/if}
  </div>
{/if}

<style>
  .alert {
    padding: 1rem;
    border-radius: 4px;
    margin-bottom: 1rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .alert-content {
    flex: 1;
  }
  
  .alert-close {
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    padding: 0;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.7;
  }
  
  .alert-close:hover {
    opacity: 1;
  }
  
  .alert-info {
    background: #cfe2ff;
    color: #084298;
    border: 1px solid #b6d4fe;
  }
  
  .alert-success {
    background: #d1e7dd;
    color: #0f5132;
    border: 1px solid #badbcc;
  }
  
  .alert-warning {
    background: #fff3cd;
    color: #856404;
    border: 1px solid #ffecb5;
  }
  
  .alert-error {
    background: #f8d7da;
    color: #842029;
    border: 1px solid #f5c2c7;
  }
</style>
svelte

SimpleFormFieldコンポーネント

<!-- SimpleFormField.svelte -->
<script lang="ts">
  type Props = {
    label: string;
    error?: string;
    helpText?: string;
    required?: boolean;
    value: string;
    type?: string;
    placeholder?: string;
  };
  
  let {
    label,
    error,
    helpText,
    required = false,
    value = $bindable(''),
    type = 'text',
    placeholder = ''
  }: Props = $props();
</script>

<div class="form-field">
  <label class="form-label">
    {label}
    {#if required}
      <span class="required">*</span>
    {/if}
  </label>
  
  <input
    {type}
    bind:value
    {placeholder}
    class="form-input"
    class:error={!!error}
    aria-invalid={!!error}
    aria-describedby={error ? 'error-message' : helpText ? 'help-text' : undefined}
  />
  
  {#if error}
    <span id="error-message" class="error-text">{error}</span>
  {:else if helpText}
    <span id="help-text" class="help-text">{helpText}</span>
  {/if}
</div>

<style>
  .form-field {
    margin-bottom: 1.5rem;
  }
  
  .form-label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
    color: #333;
  }
  
  .required {
    color: #dc3545;
  }
  
  .form-input {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
  }
  
  .form-input:focus {
    outline: none;
    border-color: #ff3e00;
    box-shadow: 0 0 0 2px rgba(255, 62, 0, 0.1);
  }
  
  .form-input.error {
    border-color: #dc3545;
  }
  
  .error-text {
    display: block;
    color: #dc3545;
    font-size: 0.875rem;
    margin-top: 0.25rem;
  }
  
  .help-text {
    display: block;
    color: #6c757d;
    font-size: 0.875rem;
    margin-top: 0.25rem;
  }
</style>
svelte

実践例:コード例

カスタムInputコンポーネント

// Input.svelte
import type { HTMLInputAttributes } from 'svelte/elements';

type Props = HTMLInputAttributes & {
  label?: string;
  error?: string;
  helpText?: string;
};

let { 
  label,
  error,
  helpText,
  value = '',
  type = 'text',
  required = false,
  ...restProps
}: Props = $props();
typescript
<div class="form-field">
  {#if label}
    <label>
      {label}
      {#if required}
        <span class="required">*</span>
      {/if}
    </label>
  {/if}
  
  <input
    {type}
    {value}
    {...restProps}
    class:error={!!error}
    aria-invalid={!!error}
    aria-describedby={error ? 'error-message' : undefined}
  />
  
  {#if error}
    <span id="error-message" class="error-text">{error}</span>
  {:else if helpText}
    <span class="help-text">{helpText}</span>
  {/if}
</div>
svelte

モーダルコンポーネント

// Modal.svelte
import type { Snippet } from 'svelte';

type Props = {
  open: boolean;
  title?: string;
  closable?: boolean;
  onClose?: () => void;
  header?: Snippet;
  footer?: Snippet;
  children: Snippet;
};

let { 
  open,
  title,
  closable = true,
  onClose,
  header,
  footer,
  children
}: Props = $props();

function handleBackdropClick(event: MouseEvent) {
  if (closable && event.target === event.currentTarget) {
    onClose?.();
  }
}

function handleEscape(event: KeyboardEvent) {
  if (closable && event.key === 'Escape') {
    onClose?.();
  }
}
typescript

データテーブル

// DataTable.svelte
type Column<T> = {
  key: keyof T;
  label: string;
  sortable?: boolean;
  width?: string;
  render?: (value: T[keyof T], row: T) => string;
};

type Props<T> = {
  data: T[];
  columns: Column<T>[];
  selectable?: boolean;
  onSelect?: (selected: T[]) => void;
  loading?: boolean;
};

let { 
  data,
  columns,
  selectable = false,
  onSelect,
  loading = false
}: Props<T> = $props();

let selected = $state<Set<T>>(new Set());
let sortColumn = $state<keyof T | null>(null);
let sortDirection = $state<'asc' | 'desc'>('asc');

// ソート済みデータ
let sortedData = $derived(() => {
  if (!sortColumn) return data;
  
  return [...data].sort((a, b) => {
    const aVal = a[sortColumn];
    const bVal = b[sortColumn];
    
    if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
    if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
    return 0;
  });
});
typescript

型のエクスポート

コンポーネントのPropsの型を他のファイルで使用できるようにエクスポートします。

// Button.svelte
export type ButtonProps = {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
};

let props: ButtonProps = $props();
typescript
// 他のファイルで使用
import type { ButtonProps } from './Button.svelte';

function createButtonProps(): ButtonProps {
  return {
    variant: 'primary',
    size: 'medium'
  };
}
typescript

ベストプラクティス

1. 型定義を明確に

// ❌ 型定義なし
let props = $props();

// ✅ 明確な型定義
type Props = {
  value: string;
  onChange: (value: string) => void;
};
let props: Props = $props();
typescript

2. デフォルト値の設定

// ✅ デフォルト値で安全に
let { 
  items = [],
  count = 0,
  enabled = true
}: Props = $props();
typescript

3. オプショナルプロパティの処理

type Props = {
  onSave?: (data: Data) => void;
};

let { onSave }: Props = $props();

function handleSave(data: Data) {
  // オプショナルチェーン演算子を使用
  onSave?.(data);
}
typescript

4. 不変性の維持

// ❌ propsを直接変更
let { items }: Props = $props();
items.push(newItem); // エラー

// ✅ 新しい配列を作成
let localItems = $state([...items]);
localItems.push(newItem);
typescript

次のステップ

プロパティの受け渡しを理解したら、 $bindable - 双方向バインディング で双方向データバインディングを学びましょう。

Last update at: 2025/08/14 07:39:43