🙌

Asset Tokenizationプロジェクト5日目(フロントエンドの実装)

に公開

Vite + React + TypeScript + Viem + Wagmiでフロントエンド構築

日付: 2026年1月5日
学習内容: モダンなWeb3フロントエンドの構築、Vite/React/TypeScript/Viem/Wagmiの統合、トークンセールDAppの実装

1. モダンなWeb3フロントエンドスタックの概要

1.1 使用する技術スタック

本プロジェクトでは、以下の最新技術を使用してトークンセールのフロントエンドを構築します:

技術 役割 特徴
Vite ビルドツール 高速な開発サーバー、最適化されたビルド
React UIフレームワーク コンポーネントベース、豊富なエコシステム
TypeScript 型システム 型安全性、優れた開発体験
Viem Ethereumライブラリ 軽量、型安全、モジュラー設計
Wagmi Reactフック viemベース、宣言的なWeb3統合
Vitest テストフレームワーク Viteネイティブ、高速実行

1.2 従来のスタックとの比較

従来のスタック(Web3.js + Truffle + Webpack):

Web3.js → 大きなバンドルサイズ
Webpack → 遅いビルド時間
JavaScript → 型チェックなし

新しいスタック(Viem + Wagmi + Vite):

Viem → 小さいバンドルサイズ、Tree-shakable
Vite → 超高速なHMR(Hot Module Replacement)
TypeScript → コンパイル時の型チェック

2. 各技術の詳細解説

2.1 Vite - 次世代フロントエンドビルドツール

Viteとは

Vite(フランス語で「速い」の意味)は、Evan You(Vue.jsの作者)によって作成された、高速なフロントエンド開発ツールです。

主な特徴:

  1. ネイティブESM: ブラウザのネイティブESモジュールを利用
  2. 高速なHMR: 変更を即座に反映
  3. 最適化されたビルド: Rollupベースのプロダクションビルド
  4. プラグインエコシステム: Rollupプラグインとの互換性

用語解説:

1. ESモジュール(ESM)とは:
ESモジュール(ECMAScript Modules)は、JavaScriptの標準的なモジュールシステムです。importexport文を使用して、コードをモジュール単位で分割し、再利用可能にします。

// モジュールのエクスポート(module.js)
export function add(a, b) {
  return a + b;
}

// モジュールのインポート(main.js)
import { add } from './module.js';
console.log(add(1, 2)); // 3

従来の方法との違い:

  • CommonJS(Node.js): require()module.exportsを使用(例: const fs = require('fs')
  • ESモジュール: importexportを使用(例: import fs from 'fs'

ViteでのネイティブESMの利点:

  • 開発モードでは、ブラウザが直接ESモジュールを読み込むため、事前のバンドルが不要
  • 起動が非常に高速(全ファイルをバンドルする必要がない)
  • 変更されたモジュールのみを再読み込みできるため、HMRが高速

2. HMR(Hot Module Replacement)とは:
HMR(ホットモジュールリプレースメント)は、開発中にコードを変更した際に、ページ全体をリロードせずに変更箇所だけを更新する機能です。

従来の開発フロー:

コード変更 → ページ全体をリロード → 状態がリセットされる

HMRを使った開発フロー:

コード変更 → 変更されたモジュールのみ更新 → 状態が保持される

HMRの利点:

  • ページのリロードが不要なため、開発が高速
  • フォーム入力やスクロール位置などの状態が保持される
  • エラーの修正が即座に反映される

:

// App.tsxを編集
function App() {
  return <div>Hello World</div>; // ← これを変更
}

// ブラウザは自動的にこの部分だけを更新
// ページ全体のリロードは発生しない

3. Rollupとは:
Rollupは、JavaScriptモジュールをバンドル(1つまたは複数のファイルにまとめる)するためのビルドツールです。ESモジュールを前提として設計されており、Tree-shaking(未使用コードの削除)に優れています。

Rollupの主な特徴:

  • Tree-shaking: 使用されていないコードを自動的に削除し、バンドルサイズを最小化
  • ESモジュール対応: モジュール形式を維持したままバンドル
  • プラグインシステム: 様々な機能をプラグインで拡張可能

Tree-shakingの例:

// utils.js
export function usedFunction() {
  return 'used';
}

export function unusedFunction() {
  return 'unused';
}

// main.js
import { usedFunction } from './utils.js';
console.log(usedFunction());

// Rollupでバンドルすると...
// unusedFunctionは使用されていないため、バンドルから除外される

ViteでのRollupの使用:

  • 開発モード: ネイティブESMを使用(Rollupは使用しない)
  • プロダクションモード: Rollupを使用して最適化されたバンドルを生成

他のバンドラーとの比較:

  • Webpack: より複雑な設定が可能だが、Tree-shakingがRollupほど効率的でない場合がある
  • Rollup: シンプルで高速、Tree-shakingに優れる(ライブラリ開発に適している)
  • esbuild: 非常に高速だが、プラグインエコシステムが限定的

なぜViteが速いのか:

従来のバンドラー(Webpack):
起動時に全ファイルをバンドル → 遅い起動
変更時に再バンドル → 遅いHMR

Vite:
起動時はバンドル不要 → 即座に起動
変更時は該当モジュールのみ更新 → 高速HMR

Viteの動作原理:

// 開発モード
import { something } from './module.js'
// ↓ ブラウザが直接ESモジュールとしてロード(バンドル不要)

// プロダクションモード
// ↓ Rollupで最適化してバンドル

Viteでサポートされる機能

標準機能:

  • TypeScript(設定不要)
  • JSX/TSX
  • CSS/SCSS/Less
  • 静的アセット処理
  • JSON import
  • Web Workers
  • WebAssembly

各機能の詳細解説:

1. JavaScriptのサポート:
Viteは、JavaScript(ES6+)を完全にサポートしています。設定不要で、.jsファイルをそのまま使用できます。

// app.js - そのまま動作
export function greet(name) {
  return `Hello, ${name}!`;
}

// main.js
import { greet } from './app.js';
console.log(greet('World')); // Hello, World!

JavaScriptとTypeScriptの違い:

  • JavaScript(.js): 動的型付け、実行時にエラーが発見される
  • TypeScript(.ts): 静的型付け、コンパイル時にエラーが発見される

ViteでのJavaScript使用例:

// utils.js
export function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// app.js
import { calculateTotal } from './utils.js';
const total = calculateTotal([{ price: 10 }, { price: 20 }]);
console.log(total); // 30

2. JSX/TSXとは:
JSX(JavaScript XML)とTSX(TypeScript XML)は、JavaScript/TypeScript内でHTMLライクな構文を記述できる拡張構文です。Reactコンポーネントを記述する際に使用されます。

JSXの基本構文:

// App.jsx
function App() {
  const name = 'World';
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <button onClick={() => alert('Clicked!')}>
        Click me
      </button>
    </div>
  );
}

TSXの基本構文:

// App.tsx
interface Props {
  name: string;
  age: number;
}

function App({ name, age }: Props) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Age: {age}</p>
    </div>
  );
}

JSXとTSXの違い:

  • JSX(.jsx): JavaScriptファイルでJSXを使用
  • TSX(.tsx): TypeScriptファイルでJSXを使用(型チェックが可能)

ViteでのJSX/TSXの使用:

// Viteは自動的にJSX/TSXをトランスパイル
// vite.config.tsでReactプラグインを設定
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()], // JSX/TSXをサポート
});

3. 静的アセット処理とは:
静的アセットとは、画像、フォント、動画などの、コードではないファイルのことです。Viteは、これらのファイルを自動的に処理し、適切なパスに配置します。

静的アセットの種類:

  • 画像: .png, .jpg, .jpeg, .gif, .svg, .webp
  • フォント: .woff, .woff2, .ttf, .otf
  • 動画・音声: .mp4, .webm, .mp3, .wav
  • その他: .pdf, .zipなど

Viteでの静的アセットの使用方法:

方法1: import文でインポート:

// 画像をインポート
import logo from './assets/logo.png';
import icon from './assets/icon.svg';

function App() {
  return (
    <div>
      <img src={logo} alt="Logo" />
      <img src={icon} alt="Icon" />
    </div>
  );
}

方法2: publicディレクトリに配置:

public/
  ├── favicon.ico
  └── images/
      └── hero.jpg
<!-- index.htmlで直接参照 -->
<img src="/images/hero.jpg" alt="Hero" />

方法3: 動的なインポート:

const imageUrl = new URL('./assets/image.png', import.meta.url).href;

Viteの静的アセット処理の特徴:

  • 自動的な最適化: 画像の圧縮や最適化
  • ハッシュ付きファイル名: キャッシュバスティング(logo.a1b2c3.png
  • 小さいファイルのインライン化: 小さな画像はBase64として埋め込まれる
  • アセットのサイズ制限: デフォルトで4KB以下のファイルはインライン化

4. JSON importとは:
JSON importは、JSONファイルをJavaScript/TypeScriptモジュールとして直接インポートできる機能です。設定ファイルやデータファイルを簡単に読み込めます。

JSON importの使用方法:

// config.json
{
  "appName": "My App",
  "version": "1.0.0",
  "features": {
    "darkMode": true,
    "notifications": false
  }
}

// app.ts
import config from './config.json';

console.log(config.appName); // "My App"
console.log(config.version); // "1.0.0"
console.log(config.features.darkMode); // true

JSON importの用途:

  • 設定ファイル: アプリケーションの設定を管理
  • データファイル: 初期データやマスターデータを読み込み
  • 多言語対応: 翻訳データをJSONで管理

実用例:

// translations/ja.json
{
  "welcome": "ようこそ",
  "goodbye": "さようなら"
}

// translations/en.json
{
  "welcome": "Welcome",
  "goodbye": "Goodbye"
}

// App.tsx
import jaTranslations from './translations/ja.json';
import enTranslations from './translations/en.json';

const translations = {
  ja: jaTranslations,
  en: enTranslations
};

function App() {
  const lang = 'ja';
  return <h1>{translations[lang].welcome}</h1>;
}

型安全性の確保:

// config.d.ts
declare module '*.json' {
  const value: any;
  export default value;
}

// または、より厳密に
interface Config {
  appName: string;
  version: string;
  features: {
    darkMode: boolean;
    notifications: boolean;
  };
}

declare module './config.json' {
  const config: Config;
  export default config;
}

5. Web Workersとは:
Web Workersは、ブラウザのメインスレッドとは別のバックグラウンドスレッドでJavaScriptコードを実行するためのAPIです。重い計算処理をメインスレッドをブロックせずに実行できます。

Web Workersの用途:

  • 重い計算処理: 画像処理、データ解析、暗号化処理
  • バックグラウンド処理: データのダウンロード、キャッシュの更新
  • UIの応答性向上: メインスレッドをブロックしない

ViteでのWeb Workersの使用方法:

// worker.ts
self.onmessage = function(e) {
  const { data } = e;
  
  // 重い計算処理
  const result = heavyCalculation(data);
  
  // 結果をメインスレッドに送信
  self.postMessage(result);
};

function heavyCalculation(data: number[]): number {
  return data.reduce((sum, num) => sum + num * num, 0);
}

// main.ts
const worker = new Worker(
  new URL('./worker.ts', import.meta.url),
  { type: 'module' }
);

worker.postMessage([1, 2, 3, 4, 5]);
worker.onmessage = (e) => {
  console.log('Result:', e.data); // Result: 55
};

Web Workersの制限事項:

  • DOMにアクセスできない
  • windowオブジェクトにアクセスできない
  • メインスレッドとメッセージパッシングで通信する必要がある

制限事項の詳細解説:

1. DOMにアクセスできない理由と影響:

なぜDOMにアクセスできないのか:
Web Workersは、メインスレッドとは完全に分離された別スレッドで実行されます。DOM(Document Object Model)はメインスレッドでのみ操作可能なため、Web Workersから直接DOMにアクセスすることはできません。これは、マルチスレッド環境での競合状態(race condition)を防ぐための設計です。

DOMアクセスを試みた場合のエラー:

// worker.ts
// ❌ これはエラーになる
const element = document.getElementById('myElement'); 
// Error: ReferenceError: document is not defined

const button = document.querySelector('button');
// Error: ReferenceError: document is not defined

element.innerHTML = 'Hello';
// Error: ReferenceError: element is not defined

DOM操作が必要な場合の対処法:
DOM操作が必要な場合は、Web Workerで計算処理を行い、結果をメインスレッドに送信して、メインスレッドでDOMを更新します。

// worker.ts - 計算処理のみ
self.onmessage = function(e) {
  const { data } = e;
  const result = heavyCalculation(data);
  
  // 結果をメインスレッドに送信
  self.postMessage({ result });
};

function heavyCalculation(data: number[]): number {
  // 重い計算処理
  return data.reduce((sum, num) => sum + num * num, 0);
}

// main.ts - DOM操作はメインスレッドで
const worker = new Worker(new URL('./worker.ts', import.meta.url));

worker.postMessage([1, 2, 3, 4, 5]);
worker.onmessage = (e) => {
  const { result } = e.data;
  
  // ✅ メインスレッドでDOMを更新
  const resultElement = document.getElementById('result');
  if (resultElement) {
    resultElement.textContent = `Result: ${result}`;
  }
  
  // ✅ ボタンの状態を更新
  const button = document.querySelector('button');
  if (button) {
    button.disabled = false;
    button.textContent = 'Calculate';
  }
};

実用例: 進捗表示の更新:

// worker.ts
self.onmessage = function(e) {
  const { items } = e.data;
  
  items.forEach((item, index) => {
    // 各アイテムを処理
    const processed = processItem(item);
    
    // 進捗をメインスレッドに送信
    self.postMessage({
      type: 'progress',
      progress: ((index + 1) / items.length) * 100,
      result: processed
    });
  });
  
  self.postMessage({ type: 'complete' });
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));

worker.onmessage = (e) => {
  const { type, progress, result } = e.data;
  
  if (type === 'progress') {
    // ✅ メインスレッドでプログレスバーを更新
    const progressBar = document.getElementById('progress-bar');
    if (progressBar) {
      progressBar.style.width = `${progress}%`;
    }
    
    // ✅ 結果をリストに追加
    const resultList = document.getElementById('result-list');
    if (resultList) {
      const li = document.createElement('li');
      li.textContent = result;
      resultList.appendChild(li);
    }
  } else if (type === 'complete') {
    // ✅ 完了メッセージを表示
    const status = document.getElementById('status');
    if (status) {
      status.textContent = 'Processing complete!';
    }
  }
};

2. windowオブジェクトにアクセスできない理由と影響:

なぜwindowオブジェクトにアクセスできないのか:
windowオブジェクトは、ブラウザのグローバルスコープを表し、メインスレッドでのみ利用可能です。Web Workersは独立したグローバルスコープ(self)を持っているため、windowオブジェクトにはアクセスできません。

windowオブジェクトへのアクセスを試みた場合のエラー:

// worker.ts
// ❌ これらはすべてエラーになる
const location = window.location;
// Error: ReferenceError: window is not defined

const localStorage = window.localStorage;
// Error: ReferenceError: window is not defined

window.alert('Hello');
// Error: ReferenceError: window is not defined

const fetch = window.fetch;
// Error: ReferenceError: window is not defined

Web Workersで利用可能なグローバルオブジェクト:
Web Workersでは、windowの代わりにselfを使用し、一部のAPIは利用可能です。

// worker.ts
// ✅ 利用可能なグローバルオブジェクト
console.log(self); // WorkerGlobalScope

// ✅ fetch APIは利用可能
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    self.postMessage({ data });
  });

// ✅ setTimeout/setIntervalは利用可能
setTimeout(() => {
  self.postMessage({ type: 'timeout' });
}, 1000);

// ✅ WebSocketは利用可能
const ws = new WebSocket('wss://example.com');
ws.onmessage = (event) => {
  self.postMessage({ type: 'websocket', data: event.data });
};

// ✅ IndexedDBは利用可能
const request = indexedDB.open('myDatabase');
request.onsuccess = (event) => {
  const db = event.target.result;
  self.postMessage({ type: 'dbReady', db });
};

windowオブジェクトの機能が必要な場合の対処法:

1. ローカルストレージの代替:

// ❌ worker.ts - これは動作しない
const data = window.localStorage.getItem('key');

// ✅ 代替案1: IndexedDBを使用
// worker.ts
const request = indexedDB.open('myDB');
request.onsuccess = (event) => {
  const db = event.target.result;
  const transaction = db.transaction(['store'], 'readonly');
  const store = transaction.objectStore('store');
  const getRequest = store.get('key');
  getRequest.onsuccess = () => {
    self.postMessage({ data: getRequest.result });
  };
};

// ✅ 代替案2: メインスレッドにリクエストを送信
// worker.ts
self.postMessage({ type: 'getStorage', key: 'myKey' });

// main.ts
worker.onmessage = (e) => {
  if (e.data.type === 'getStorage') {
    const value = localStorage.getItem(e.data.key);
    worker.postMessage({ type: 'storageValue', value });
  }
};

2. ロケーション情報の取得:

// ❌ worker.ts - これは動作しない
const url = window.location.href;

// ✅ 代替案: メインスレッドから情報を送信
// main.ts
worker.postMessage({ 
  type: 'init',
  location: window.location.href,
  userAgent: navigator.userAgent
});

// worker.ts
let appLocation: string;
self.onmessage = (e) => {
  if (e.data.type === 'init') {
    appLocation = e.data.location;
    // これでロケーション情報を使用可能
  }
};

3. アラートやダイアログの表示:

// ❌ worker.ts - これは動作しない
window.alert('Processing complete!');

// ✅ 代替案: メインスレッドに通知を送信
// worker.ts
self.postMessage({ 
  type: 'showAlert', 
  message: 'Processing complete!' 
});

// main.ts
worker.onmessage = (e) => {
  if (e.data.type === 'showAlert') {
    // ✅ メインスレッドでアラートを表示
    alert(e.data.message);
    
    // または、より柔軟に
    const notification = document.createElement('div');
    notification.className = 'notification';
    notification.textContent = e.data.message;
    document.body.appendChild(notification);
    
    setTimeout(() => {
      notification.remove();
    }, 3000);
  }
};

3. メッセージパッシングによる通信の詳細:

なぜメッセージパッシングが必要なのか:
Web Workersとメインスレッドは、異なるスレッドで実行されるため、直接変数を共有することはできません。データの受け渡しには、postMessageonmessageを使用したメッセージパッシングが必要です。

メッセージパッシングの基本パターン:

// worker.ts
self.onmessage = function(e) {
  const { type, data } = e.data;
  
  switch (type) {
    case 'calculate':
      const result = calculate(data);
      self.postMessage({ type: 'result', result });
      break;
      
    case 'process':
      processData(data, (progress) => {
        self.postMessage({ type: 'progress', progress });
      });
      break;
  }
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));

// メッセージを送信
worker.postMessage({ 
  type: 'calculate', 
  data: [1, 2, 3, 4, 5] 
});

// メッセージを受信
worker.onmessage = (e) => {
  const { type, result, progress } = e.data;
  
  if (type === 'result') {
    console.log('Result:', result);
  } else if (type === 'progress') {
    updateProgressBar(progress);
  }
};

メッセージの転送可能オブジェクト(Transferable Objects):
大きなデータを効率的に転送するために、Transferable Objectsを使用できます。これにより、データのコピーではなく、所有権の転送が行われます。

// main.ts
const largeArray = new Uint8Array(1024 * 1024 * 100); // 100MB

// ✅ Transferable Objectsを使用(高速)
worker.postMessage(
  { data: largeArray },
  [largeArray.buffer] // バッファの所有権を転送
);

// ⚠️ 転送後、元の配列は使用できなくなる
// console.log(largeArray); // 空の配列になる

エラーハンドリング:

// worker.ts
self.onmessage = function(e) {
  try {
    const result = processData(e.data);
    self.postMessage({ type: 'success', result });
  } catch (error) {
    // ✅ エラーをメインスレッドに送信
    self.postMessage({ 
      type: 'error', 
      error: error.message 
    });
  }
};

// main.ts
worker.onmessage = (e) => {
  if (e.data.type === 'error') {
    console.error('Worker error:', e.data.error);
    showErrorMessage(e.data.error);
  }
};

// Worker内で発生したエラーをキャッチ
worker.onerror = (error) => {
  console.error('Worker error:', error);
};

実用例: 画像処理:

// imageWorker.ts
self.onmessage = function(e) {
  const { imageData } = e.data;
  
  // 画像のグレースケール変換
  for (let i = 0; i < imageData.data.length; i += 4) {
    const gray = imageData.data[i] * 0.299 + 
                 imageData.data[i + 1] * 0.587 + 
                 imageData.data[i + 2] * 0.114;
    imageData.data[i] = gray;
    imageData.data[i + 1] = gray;
    imageData.data[i + 2] = gray;
  }
  
  self.postMessage({ imageData });
};

6. WebAssembly(WASM)とは:
WebAssemblyは、ブラウザ上で高速に実行できるバイナリ形式の低レベル言語です。C/C++、Rust、Goなどの言語からコンパイルでき、JavaScriptよりも高速に実行できます。

WebAssemblyの特徴:

  • 高速実行: JavaScriptよりも数倍から数十倍高速
  • 低レベル: メモリを直接操作可能
  • ポータブル: 様々なプラットフォームで実行可能
  • セキュア: サンドボックス環境で実行

WebAssemblyの用途:

  • 高性能な計算: 3Dグラフィックス、物理シミュレーション
  • 既存のC/C++コードの再利用: ライブラリをブラウザで使用
  • 暗号化処理: 高速な暗号化・復号化
  • ゲームエンジン: Unity、Unreal Engineなどのゲームをブラウザで実行

ViteでのWebAssemblyの使用方法:

// main.ts
async function loadWasm() {
  const wasmModule = await import('./add.wasm');
  const result = wasmModule.add(5, 3);
  console.log(result); // 8
}

loadWasm();

RustからWebAssemblyを生成する例:

// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}
# RustをWebAssemblyにコンパイル
wasm-pack build --target web

WebAssemblyとJavaScriptの比較:

// JavaScript版(遅い)
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// WebAssembly版(高速)
// C/C++やRustで実装し、WASMにコンパイル
// JavaScriptから呼び出し可能

実用例: 画像処理ライブラリ:

// imageProcessor.wasmを使用
import init, { processImage } from './imageProcessor.wasm';

async function handleImage(imageData: ImageData) {
  await init(); // WASMモジュールを初期化
  const processed = processImage(imageData); // 高速処理
  return processed;
}

パフォーマンス最適化:

// 依存関係の事前バンドル(esbuild使用)
// node_modulesは初回のみバンドル、キャッシュ利用

// コード分割
const Component = lazy(() => import('./Component'))

// 自動的にChunkに分割してロード

2.2 React - UIライブラリ

Reactの基本概念

Reactは、Facebook(現在のMeta)によって開発されたUIライブラリで、コンポーネントベースの宣言的なUI構築を可能にします。

ReactはUIだけではなく、機能も提供します:
Reactは「UIライブラリ」と呼ばれますが、単にUIを描画するだけではありません。以下のような機能を提供します:

  1. 状態管理: コンポーネントの状態を管理し、状態変更に応じてUIを自動更新
  2. ライフサイクル管理: コンポーネントのマウント、更新、アンマウントを制御
  3. イベント処理: ユーザーインタラクション(クリック、入力など)を処理
  4. パフォーマンス最適化: 仮想DOMによる効率的な再レンダリング
  5. 副作用の管理: API呼び出し、タイマー、サブスクリプションなどの処理
  6. コンテキスト管理: コンポーネントツリー全体でデータを共有

Reactの特徴:

  • 宣言的: 「何を表示するか」を記述し、「どうやって表示するか」はReactが処理
  • コンポーネントベース: UIを小さな部品に分割して再利用
  • 単方向データフロー: データは親から子へ流れ、予測可能な動作
  • 仮想DOM: 実際のDOM操作を最小限に抑えてパフォーマンスを向上

コアコンセプトの詳細解説:

1. コンポーネント(Component)とは:
コンポーネントは、UIを構成する再利用可能な独立した部品です。各コンポーネントは、入力(Props)を受け取り、出力(JSX)を返します。

コンポーネントの種類:

  • 関数コンポーネント: 関数として定義(推奨)
  • クラスコンポーネント: クラスとして定義(レガシー)

関数コンポーネントの例:

// シンプルなコンポーネント
function Welcome({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>;
}

// 複雑なコンポーネント
function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// コンポーネントの再利用
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
      <UserProfile user={currentUser} />
    </div>
  );
}

コンポーネントの利点:

  • 再利用性: 同じコンポーネントを複数箇所で使用可能
  • 保守性: 変更が一箇所で済む
  • テスト容易性: 各コンポーネントを独立してテスト可能
  • 関心の分離: UIロジックを分離して管理

2. Props(Properties)とは:
Propsは、親コンポーネントから子コンポーネントへデータを渡すための仕組みです。Propsは読み取り専用で、子コンポーネント内で変更できません。

Propsの基本:

// 親コンポーネント
function App() {
  const userName = "Alice";
  const userAge = 30;
  
  return (
    <UserCard 
      name={userName} 
      age={userAge}
      isActive={true}
    />
  );
}

// 子コンポーネント
interface UserCardProps {
  name: string;
  age: number;
  isActive?: boolean; // オプショナル
}

function UserCard({ name, age, isActive = false }: UserCardProps) {
  return (
    <div className={isActive ? 'active' : 'inactive'}>
      <h3>{name}</h3>
      <p>Age: {age}</p>
    </div>
  );
}

Propsの種類:

  • プリミティブ型: string, number, boolean
  • オブジェクト: { name: string, age: number }
  • 配列: string[], number[]
  • 関数: (value: string) => void
  • React要素: React.ReactNode

関数をPropsとして渡す例:

// 親コンポーネント
function App() {
  const handleClick = (id: number) => {
    console.log(`Clicked: ${id}`);
  };
  
  return (
    <Button 
      label="Click me"
      onClick={handleClick}
      id={1}
    />
  );
}

// 子コンポーネント
interface ButtonProps {
  label: string;
  onClick: (id: number) => void;
  id: number;
}

function Button({ label, onClick, id }: ButtonProps) {
  return (
    <button onClick={() => onClick(id)}>
      {label}
    </button>
  );
}

Propsの制約:

  • 読み取り専用: Propsは変更できない(イミュータブル)
  • 単方向データフロー: 親から子へのみデータが流れる
  • 変更が必要な場合: Stateを使用する

3. State(状態)とは:
Stateは、コンポーネント内部で管理される変更可能なデータです。Stateが変更されると、Reactは自動的にコンポーネントを再レンダリングします。

Stateの基本:

import { useState } from 'react';

function Counter() {
  // Stateの宣言: [現在の値, 更新関数] = useState(初期値)
  const [count, setCount] = useState<number>(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      {/* Stateを更新する関数を呼び出す */}
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  );
}

StateとPropsの違い:

function UserProfile({ initialName }: { initialName: string }) {
  // Props: 親から受け取る(読み取り専用)
  const name = initialName; // ❌ 変更不可
  
  // State: コンポーネント内部で管理(変更可能)
  const [age, setAge] = useState<number>(0); // ✅ 変更可能
  
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
      <button onClick={() => setAge(age + 1)}>
        Increase Age
      </button>
    </div>
  );
}

複数のStateの管理:

function Form() {
  // 複数のStateを独立して管理
  const [name, setName] = useState<string>('');
  const [email, setEmail] = useState<string>('');
  const [age, setAge] = useState<number>(0);
  
  // または、オブジェクトとして管理
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: 0
  });
  
  // オブジェクトStateの更新
  const updateName = (newName: string) => {
    setFormData({ ...formData, name: newName });
  };
  
  return (
    <form>
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input 
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
    </form>
  );
}

State更新の注意点:

function Counter() {
  const [count, setCount] = useState(0);
  
  // ❌ 間違い: 直接変更
  const incrementWrong = () => {
    count = count + 1; // Stateは直接変更できない
  };
  
  // ✅ 正しい: setState関数を使用
  const incrementCorrect = () => {
    setCount(count + 1);
  };
  
  // ✅ 関数形式の更新(推奨)
  const incrementBetter = () => {
    setCount(prevCount => prevCount + 1);
  };
  
  return <button onClick={incrementBetter}>Count: {count}</button>;
}

4. Hooks(フック)とは:
Hooksは、関数コンポーネントで状態管理や副作用処理を行うためのReactの機能です。クラスコンポーネントの機能を関数コンポーネントでも使用できるようにします。

Hooksの基本ルール:

  1. トップレベルでのみ呼び出す: 条件分岐やループ内では呼び出さない
  2. 関数コンポーネント内でのみ使用: 通常のJavaScript関数では使用できない
  3. カスタムHooksはuseで始める: useCustomHookのように命名

主要なHooks:

useState: 状態管理

const [state, setState] = useState(initialValue);

// 例
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

useEffect: 副作用の処理

useEffect(() => {
  // マウント時・更新時に実行される処理
  // API呼び出し、イベントリスナーの登録など
  
  return () => {
    // クリーンアップ処理(オプション)
    // アンマウント時・更新前に実行
  };
}, [dependencies]); // 依存配列

// 例: API呼び出し
useEffect(() => {
  fetchUserData(userId).then(setUser);
}, [userId]); // userIdが変更されたら再実行

// 例: イベントリスナー
useEffect(() => {
  const handleResize = () => {
    setWindowWidth(window.innerWidth);
  };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []); // マウント時のみ実行

useContext: コンテキストの使用

const value = useContext(MyContext);

// 例: テーマコンテキスト
const theme = useContext(ThemeContext);
return <div className={theme === 'dark' ? 'dark' : 'light'}>...</div>;

useCallback: 関数のメモ化

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]); // aまたはbが変更されたら再作成

// 例: 子コンポーネントに渡す関数をメモ化
const handleClick = useCallback((id: number) => {
  console.log(`Clicked: ${id}`);
}, []); // 依存がないので一度だけ作成

useMemo: 値のメモ化

const memoizedValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]); // aまたはbが変更されたら再計算

// 例: 重い計算をメモ化
const expensiveResult = useMemo(() => {
  return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);

useRef: 可変な値を保持

const ref = useRef(initialValue);

// 例: DOM要素への参照
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />;
inputRef.current?.focus(); // フォーカスを設定

// 例: 前回の値を保持
const prevCountRef = useRef<number>();
useEffect(() => {
  prevCountRef.current = count;
});

Hooksの実用例:

function TokenBalance({ address }: { address: string }) {
  // State: トークン残高を管理
  const [balance, setBalance] = useState<bigint>(0n);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  
  // useEffect: アドレスが変更されたら残高を取得
  useEffect(() => {
    if (!address) return;
    
    setIsLoading(true);
    fetchBalance(address)
      .then(setBalance)
      .finally(() => setIsLoading(false));
  }, [address]); // addressが変更されたら再実行
  
  // useMemo: フォーマット済み残高をメモ化
  const formattedBalance = useMemo(() => {
    return formatEther(balance);
  }, [balance]);
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  return <div>Balance: {formattedBalance} ETH</div>;
}

Reactが提供する機能のまとめ:

  • UIレンダリング: JSXをDOMに変換
  • 状態管理: useState、useReducer
  • ライフサイクル: useEffect、useLayoutEffect
  • パフォーマンス最適化: useMemo、useCallback、React.memo
  • コンテキスト: useContext、createContext
  • 参照管理: useRef
  • カスタムHooks: 独自のロジックを再利用可能に

関数コンポーネントの例:

import { useState, useEffect } from 'react';

function TokenBalance({ address }: { address: string }) {
  const [balance, setBalance] = useState<bigint>(0n);

  useEffect(() => {
    // アドレスが変更されたら残高を取得
    fetchBalance(address).then(setBalance);
  }, [address]);

  return <div>Balance: {balance.toString()}</div>;
}

主要なReact Hooks:

// useState: 状態管理
const [count, setCount] = useState(0);

// useEffect: 副作用の処理
useEffect(() => {
  // マウント時・更新時に実行
  return () => {
    // アンマウント時のクリーンアップ
  };
}, [dependencies]);

// useCallback: 関数のメモ化
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// useMemo: 値のメモ化
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

2.3 TypeScript - 型安全なJavaScript

TypeScriptの利点

TypeScriptは、JavaScriptに静的型システムを追加したスーパーセットです。

主な利点:

  1. 型安全性: コンパイル時にエラーを検出
  2. 優れたIDE支援: 自動補完、リファクタリング
  3. ドキュメント: 型が仕様を表現
  4. リファクタリング: 安全な大規模変更

リファクタリングとは:
リファクタリング(Refactoring)は、コードの動作を変えずに、コードの構造や品質を改善する作業です。TypeScriptでは、型情報を活用して安全にリファクタリングできます。

リファクタリングの種類と例:

1. 変数名の変更(Rename Symbol):

// 変更前
function getUserBalance(userAddress: string): bigint {
  // ...
}

// 変数名を変更したい場合
const addr = '0x123...';
const bal = getUserBalance(addr);

// ✅ TypeScriptのリファクタリング機能を使用
// 1. `addr`を右クリック → "Rename Symbol"
// 2. `address`に変更
// 3. すべての使用箇所が自動的に更新される

// 変更後(自動的に更新)
const address = '0x123...';
const balance = getUserBalance(address);

2. 関数の抽出(Extract Function):

// 変更前: 長い関数
function processTransaction(tx: Transaction) {
  // 複雑な処理...
  const hash = keccak256(abi.encode(tx));
  const signature = sign(hash, privateKey);
  const verified = verify(signature, hash, publicKey);
  // ...
}

// ✅ リファクタリング: 関数を抽出
function hashTransaction(tx: Transaction): string {
  return keccak256(abi.encode(tx));
}

function signTransaction(hash: string, privateKey: string): string {
  return sign(hash, privateKey);
}

function verifyTransaction(signature: string, hash: string, publicKey: string): boolean {
  return verify(signature, hash, publicKey);
}

// 変更後: 読みやすくなった
function processTransaction(tx: Transaction) {
  const hash = hashTransaction(tx);
  const signature = signTransaction(hash, privateKey);
  const verified = verifyTransaction(signature, hash, publicKey);
  // ...
}

3. インターフェースの変更(Change Signature):

// 変更前
interface User {
  name: string;
  age: number;
}

function greetUser(user: User): string {
  return `Hello, ${user.name}!`;
}

// ✅ リファクタリング: プロパティを追加
interface User {
  name: string;
  age: number;
  email: string; // 新しいプロパティを追加
}

// TypeScriptが自動的にエラーを検出
// すべてのUserオブジェクトにemailを追加する必要がある

4. 型の統一(Find All References):

// 変更前: 同じ意味の異なる型が混在
type Address = string;
type UserAddress = string;
type WalletAddress = string;

function sendToken(to: Address, amount: bigint) { }
function getUser(address: UserAddress) { }
function getBalance(addr: WalletAddress) { }

// ✅ リファクタリング: 型を統一
type Address = `0x${string}`; // より厳密な型

// "Find All References"で使用箇所を確認
// すべての型を統一して変更
function sendToken(to: Address, amount: bigint) { }
function getUser(address: Address) { }
function getBalance(addr: Address) { }

5. コードの移動(Move Symbol):

// 変更前: utils.ts
export function formatEther(value: bigint): string {
  return (Number(value) / 1e18).toFixed(4);
}

// ✅ リファクタリング: より適切な場所に移動
// ether.tsに移動
// すべてのインポートが自動的に更新される

TypeScriptによる安全なリファクタリングの利点:

1. 型チェックによる安全性:

// リファクタリング前
function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// リファクタリング: 戻り値の型を変更
function calculateTotal(items: Item[]): bigint {
  return items.reduce((sum, item) => sum + BigInt(item.price), 0n);
}

// ✅ TypeScriptが自動的にエラーを検出
// この関数を使用しているすべての箇所で型エラーが発生
// リファクタリングが不完全であることを即座に発見可能

2. 自動的な影響範囲の特定:

// 関数名を変更する場合
function getUserBalance(address: string): bigint { }

// ✅ IDEが自動的に使用箇所をハイライト
// すべての使用箇所を確認してから変更可能
// 見落としによるバグを防止

3. 一括変更の安全性:

// 変更前
interface Config {
  apiUrl: string;
  timeout: number;
}

// ✅ プロパティ名を変更
interface Config {
  apiEndpoint: string; // apiUrl → apiEndpoint
  requestTimeout: number; // timeout → requestTimeout
}

// TypeScriptがすべての使用箇所を検出
// 一括で変更可能で、見落としがない

4. リファクタリングの例: 大規模な変更:

// 変更前: 文字列でアドレスを管理
function transferToken(from: string, to: string, amount: string) {
  // ...
}

// ✅ リファクタリング: 型安全なアドレス型を使用
type Address = `0x${string}`;
type Amount = bigint;

function transferToken(from: Address, to: Address, amount: Amount) {
  // ...
}

// TypeScriptがすべての呼び出し箇所を検出
// 型エラーを確認しながら段階的に修正可能

IDEでのリファクタリング機能:

  • Rename Symbol (F2): 変数、関数、型などの名前を変更
  • Extract Function: 選択したコードを関数として抽出
  • Extract Variable: 式を変数として抽出
  • Change Signature: 関数のシグネチャ(引数、戻り値)を変更
  • Move Symbol: シンボルを別のファイルに移動
  • Find All References (Shift+F12): すべての使用箇所を検索
  • Go to Definition (F12): 定義箇所に移動

型の例:

// プリミティブ型
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;

// 配列型
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];

// オブジェクト型
interface User {
  name: string;
  age: number;
  email?: string; // オプショナル
}

// 関数型
type CalculateFunction = (a: number, b: number) => number;

// ジェネリック型
function identity<T>(arg: T): T {
  return arg;
}

// ユニオン型
type Status = "pending" | "success" | "error";

// インターセクション型
type UserWithTimestamp = User & { createdAt: Date };

Web3開発でのTypeScriptの重要性:

// ❌ JavaScriptでは実行時エラー
const balance = await contract.balanceOf(address);
const doubled = balance * 2; // エラー: BigIntは*演算子使えない

// ✅ TypeScriptでコンパイル時に検出
const balance: bigint = await contract.balanceOf(address);
const doubled = balance * 2n; // 型エラーで事前に検出

2.4 Viem - モダンなEthereumライブラリ

Viemとは

Viemは、型安全で軽量なEthereumライブラリです。Web3.jsやEthers.jsの後継として設計されています。

主な特徴:

  1. 型安全: TypeScript完全対応
  2. 軽量: Tree-shakableで必要な機能のみバンドル
  3. モジュラー: 機能ごとに分割されたAPI
  4. 高性能: 最適化されたJSON-RPC通信

Tree-shakableとは:
Tree-shaking(ツリーシェイキング)は、使用されていないコードをバンドルから自動的に削除する最適化技術です。ViemはTree-shakableな設計により、必要な機能のみをバンドルに含めることができ、バンドルサイズを大幅に削減できます。

Tree-shakingの仕組み:

// Viemのモジュール構造
import { createPublicClient, http } from 'viem';
import { parseEther } from 'viem';

// ✅ 使用した機能のみがバンドルに含まれる
// createPublicClient, http, parseEther のみが含まれる

// ❌ 使用していない機能はバンドルから除外される
// 例: formatEther, parseUnits などは含まれない

Tree-shakingの例:

1. 必要な機能のみをインポート:

// ✅ Tree-shakable: 必要な機能のみインポート
import { createPublicClient, http } from 'viem';
import { parseEther } from 'viem';

// バンドルサイズ: 小さい(必要な機能のみ)

// ❌ Tree-shakableでない: 全体をインポート
import * as viem from 'viem';

// バンドルサイズ: 大きい(すべての機能が含まれる)

2. 使用されていないコードの削除:

// utils.ts
export function usedFunction() {
  return 'used';
}

export function unusedFunction() {
  return 'unused';
}

// main.ts
import { usedFunction } from './utils';

// ✅ Tree-shaking後: unusedFunctionはバンドルから削除される
// バンドルには usedFunction のみが含まれる

ViemでのTree-shakingの利点:

1. バンドルサイズの削減:

// Web3.jsの場合(Tree-shakableでない)
import Web3 from 'web3';
const web3 = new Web3();

// バンドルサイズ: 約800KB(すべての機能が含まれる)
// 使用していない機能も含まれてしまう

// Viemの場合(Tree-shakable)
import { createPublicClient, http } from 'viem';
import { parseEther } from 'viem';

// バンドルサイズ: 約50KB(必要な機能のみ)
// 使用していない機能は自動的に除外される

2. 必要な機能のみを選択:

// パブリッククライアントのみ必要な場合
import { createPublicClient, http } from 'viem';
// ✅ バンドルサイズ: 最小限(読み取り機能のみ)

// ウォレット機能も必要な場合
import { createPublicClient, http } from 'viem';
import { createWalletClient, custom } from 'viem';
// ✅ バンドルサイズ: 必要な機能のみ追加される

// ユーティリティ関数も必要な場合
import { parseEther, formatEther } from 'viem';
// ✅ 必要なユーティリティのみが追加される

3. モジュラー設計による最適化:

// Viemのモジュラー設計
import { createPublicClient } from 'viem';
import { http } from 'viem';
import { mainnet } from 'viem/chains';
import { parseEther } from 'viem';

// 各機能が独立したモジュールとして提供される
// 使用したモジュールのみがバンドルに含まれる

// チェーン情報も必要な場合のみインポート
import { mainnet, sepolia, polygon } from 'viem/chains';
// ✅ 使用したチェーンの情報のみが含まれる

Tree-shakingが機能するための条件:

1. ESモジュール形式:

// ✅ Tree-shakable: ESモジュール形式
export function function1() { }
export function function2() { }

// ❌ Tree-shakableでない: CommonJS形式
module.exports = {
  function1: function() { },
  function2: function() { }
};

2. 副作用のないコード:

// ✅ Tree-shakable: 副作用がない
export function add(a: number, b: number): number {
  return a + b;
}

// ❌ Tree-shakableでない: 副作用がある
export function add(a: number, b: number): number {
  console.log('Adding...'); // 副作用
  window.globalVar = a + b; // 副作用
  return a + b;
}

3. 静的解析可能なインポート:

// ✅ Tree-shakable: 静的解析可能
import { createPublicClient } from 'viem';

// ❌ Tree-shakableでない: 動的インポート(条件付き)
if (condition) {
  const { createPublicClient } = await import('viem');
}

ViemのTree-shaking実装例:

最小限のバンドル:

// 最小限の機能のみ使用
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({
  chain: mainnet,
  transport: http()
});

// バンドルサイズ: 約30KB
// 含まれる機能: Public Client, HTTP Transport, Mainnet Chain

機能を追加した場合:

// ウォレット機能を追加
import { createPublicClient, http } from 'viem';
import { createWalletClient, custom } from 'viem';
import { parseEther, formatEther } from 'viem';

// バンドルサイズ: 約50KB
// 追加された機能: Wallet Client, Custom Transport, ユーティリティ関数

Tree-shakingの効果測定:

# バンドルサイズの確認
npm run build

# 出力例:
# dist/assets/index-abc123.js   416.06 kB │ gzip: 126.66 kB
# 
# Tree-shakingにより、使用していない機能は除外される
# 実際のバンドルサイズは使用する機能に応じて変動

Tree-shakingのベストプラクティス:

1. 名前付きインポートを使用:

// ✅ 推奨: 名前付きインポート
import { createPublicClient, http } from 'viem';

// ❌ 非推奨: 名前空間インポート
import * as viem from 'viem';

2. 必要な機能のみをインポート:

// ✅ 推奨: 必要な機能のみ
import { parseEther } from 'viem';

// ❌ 非推奨: すべてのユーティリティをインポート
import { parseEther, formatEther, parseUnits, formatUnits, ... } from 'viem';

3. 条件付きインポートの回避:

// ✅ 推奨: 常にインポート
import { createPublicClient } from 'viem';

// ❌ 非推奨: 条件付きインポート(Tree-shakingが効かない場合がある)
if (needed) {
  const { createPublicClient } = await import('viem');
}

Web3.js/Ethers.jsとの比較:

// Web3.js(古いスタイル)
const web3 = new Web3(window.ethereum);
const balance = await web3.eth.getBalance(address);
// 型: string(不便)

// Ethers.js v5
const provider = new ethers.providers.Web3Provider(window.ethereum);
const balance = await provider.getBalance(address);
// 型: BigNumber(独自型)

// Viem
import { createPublicClient, http } from 'viem';
const client = createPublicClient({
  transport: http()
});
const balance = await client.getBalance({ address });
// 型: bigint(ネイティブ)

Viemの主要API:

// 1. Public Client(読み取り専用)
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http()
});

// ブロック番号取得
const blockNumber = await publicClient.getBlockNumber();

// 残高取得
const balance = await publicClient.getBalance({
  address: '0x...'
});

// コントラクト読み取り
const data = await publicClient.readContract({
  address: '0x...',
  abi: contractAbi,
  functionName: 'balanceOf',
  args: ['0x...']
});

// 2. Wallet Client(書き込み)
import { createWalletClient, custom } from 'viem';

const walletClient = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum)
});

// トランザクション送信
const hash = await walletClient.writeContract({
  address: '0x...',
  abi: contractAbi,
  functionName: 'transfer',
  args: ['0x...', parseEther('1')]
});

// 3. ユーティリティ関数
import { parseEther, formatEther, parseUnits } from 'viem';

const wei = parseEther('1.5'); // 1.5 ETH → wei
const eth = formatEther(1500000000000000000n); // wei → "1.5"
const usdc = parseUnits('100', 6); // 100 USDC (6 decimals)

Viemの型安全性:

// ABIから型を自動推論
const contract = {
  address: '0x...',
  abi: [
    {
      name: 'balanceOf',
      inputs: [{ name: 'account', type: 'address' }],
      outputs: [{ name: 'balance', type: 'uint256' }],
      stateMutability: 'view',
      type: 'function'
    }
  ] as const, // as const が重要
};

// 型安全な呼び出し
const balance = await publicClient.readContract({
  ...contract,
  functionName: 'balanceOf', // 自動補完される
  args: ['0x...'] // 型チェックされる
});
// balance の型は bigint と推論される

2.5 Wagmi - React用Web3フック

Wagmiとは

Wagmiは、React用のWeb3フックコレクションで、Viemをベースに構築されています。

主な特徴:

  1. 宣言的: Reactの思想に沿ったAPI
  2. 型安全: TypeScript完全対応
  3. キャッシング: 自動的なデータキャッシュ
  4. SSR対応: Next.jsなどでも動作
  5. マルチチェーン: 複数のチェーンに対応

Wagmiのコアフック:

// 1. useAccount - アカウント情報
import { useAccount } from 'wagmi';

function Profile() {
  const { address, isConnected, chain } = useAccount();

  if (!isConnected) return <div>Not connected</div>;

  return (
    <div>
      <p>Address: {address}</p>
      <p>Chain: {chain?.name}</p>
    </div>
  );
}

// 2. useConnect - ウォレット接続
import { useConnect } from 'wagmi';

function Connect() {
  const { connect, connectors } = useConnect();

  return (
    <>
      {connectors.map((connector) => (
        <button
          key={connector.id}
          onClick={() => connect({ connector })}
        >
          Connect {connector.name}
        </button>
      ))}
    </>
  );
}

// 3. useReadContract - コントラクト読み取り
import { useReadContract } from 'wagmi';

function TokenBalance({ address }: { address: `0x${string}` }) {
  const { data: balance, isLoading } = useReadContract({
    address: '0x...',
    abi: tokenAbi,
    functionName: 'balanceOf',
    args: [address],
  });

  if (isLoading) return <div>Loading...</div>;
  return <div>Balance: {balance?.toString()}</div>;
}

// 4. useWriteContract - コントラクト書き込み
import { useWriteContract } from 'wagmi';
import { parseEther } from 'viem';

function Transfer() {
  const { writeContract, isPending } = useWriteContract();

  const handleTransfer = () => {
    writeContract({
      address: '0x...',
      abi: tokenAbi,
      functionName: 'transfer',
      args: ['0x...', parseEther('1')],
    });
  };

  return (
    <button onClick={handleTransfer} disabled={isPending}>
      {isPending ? 'Transferring...' : 'Transfer'}
    </button>
  );
}

// 5. useWatchContractEvent - イベント監視
import { useWatchContractEvent } from 'wagmi';

function EventListener() {
  useWatchContractEvent({
    address: '0x...',
    abi: tokenAbi,
    eventName: 'Transfer',
    onLogs: (logs) => {
      console.log('Transfer event:', logs);
    },
  });

  return <div>Listening for transfers...</div>;
}

Wagmiの状態管理:

// Wagmiは内部的にTanStack Query(React Query)を使用
// 自動的にキャッシュ、再取得、エラーハンドリング

const { data, isLoading, isError, refetch } = useReadContract({
  address: '0x...',
  abi: tokenAbi,
  functionName: 'balanceOf',
  args: [address],
});

// 自動リフレッシュの設定
const { data } = useReadContract({
  // ...
  query: {
    refetchInterval: 10000, // 10秒ごとに再取得
  },
});

2.6 Vitest - Viteネイティブなテストフレームワーク

Vitestとは

Vitestは、Viteと同じ設定で動作する高速なテストフレームワークです。

主な特徴:

  1. Viteネイティブ: Viteの設定を共有
  2. Jest互換: JestのAPIとほぼ同じ
  3. 高速: esbuildによる高速トランスパイル
  4. TypeScript対応: 設定不要
  5. ウォッチモード: 変更を検知して自動実行

Vitestの基本的な使用法:

// 基本的なテスト
import { describe, it, expect } from 'vitest';

describe('Calculator', () => {
  it('adds two numbers', () => {
    expect(1 + 1).toBe(2);
  });

  it('multiplies two numbers', () => {
    expect(2 * 3).toBe(6);
  });
});

// 非同期テスト
it('fetches user data', async () => {
  const data = await fetchUser('123');
  expect(data.name).toBe('Alice');
});

// モック
import { vi } from 'vitest';

it('calls callback', () => {
  const callback = vi.fn();
  someFunction(callback);
  expect(callback).toHaveBeenCalledTimes(1);
});

Reactコンポーネントのテスト:

import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import Button from './Button';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);

    await userEvent.click(screen.getByText('Click'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

3. プロジェクトのセットアップ

3.1 Vite + React + TypeScriptプロジェクトの作成

初期プロジェクト作成:

# npm create viteを使用
npm create vite@latest frontend -- --template react-ts

# プロジェクトディレクトリに移動
cd frontend

# 依存関係をインストール
npm install

生成されるプロジェクト構造:

frontend/
├── node_modules/
├── public/
│   └── vite.svg
├── src/
│   ├── assets/
│   │   └── react.svg
│   ├── App.css
│   ├── App.tsx          # メインコンポーネント
│   ├── index.css
│   └── main.tsx         # エントリーポイント
├── .gitignore
├── index.html           # HTMLテンプレート
├── package.json
├── tsconfig.json        # TypeScript設定
├── tsconfig.node.json
└── vite.config.ts       # Vite設定

package.jsonの確認:

{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",              // 開発サーバー起動
    "build": "tsc -b && vite build",  // ビルド
    "lint": "eslint .",         // Lint実行
    "preview": "vite preview"   // ビルド結果をプレビュー
  },
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  },
  "devDependencies": {
    "@types/react": "^19.2.5",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "typescript": "~5.9.3",
    "vite": "^7.2.4"
  }
}

3.2 Web3ライブラリのインストール

Viem、Wagmi、TanStack Queryのインストール:

npm install viem wagmi @wagmi/core @tanstack/react-query

インストールされるパッケージ:

  • viem: Ethereumライブラリ
  • wagmi: React用Web3フック
  • @wagmi/core: Wagmiのコア機能
  • @tanstack/react-query: データフェッチング・キャッシュ管理

package.jsonへの追加:

{
  "dependencies": {
    "@tanstack/react-query": "^5.90.16",
    "@wagmi/core": "^3.0.2",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "viem": "^2.43.5",
    "wagmi": "^3.1.4"
  }
}

3.3 Vitestとテストライブラリのインストール

テスト関連パッケージのインストール:

# Vitest本体
npm install -D vitest

# React Testing Library(コンポーネントテスト用)
npm install -D @testing-library/react @testing-library/jest-dom

# jsdom(DOMシミュレーション用)
npm install -D jsdom

インストールされるパッケージ:

  • vitest: テストランナー
  • @testing-library/react: Reactコンポーネントテスト
  • @testing-library/jest-dom: DOMマッチャー
  • jsdom: ブラウザ環境のシミュレーション

package.jsonへのテストスクリプト追加:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "test": "vitest"  // テストスクリプト追加
  }
}

3.4 Vite設定の更新

vite.config.tsの更新:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,        // グローバルなテストAPI
    environment: 'jsdom', // ブラウザ環境をシミュレート
  },
})

設定の説明:

  • plugins: [react()]: React用のViteプラグイン
  • test.globals: true: describe、it、expectをグローバルで使用可能に
  • test.environment: 'jsdom': DOMをシミュレート(Reactコンポーネントテスト用)

4. コントラクトABIの抽出

4.1 FoundryアーティファクトからのABI抽出

FoundryのoutputディレクトリからABIを抽出:

# contractsディレクトリを作成
mkdir -p src/contracts

# Node.jsを使用してABIを抽出(jqが不要)
node -e "
const fs = require('fs');
const myToken = JSON.parse(fs.readFileSync('../out/MyToken.sol/MyToken.json', 'utf8'));
fs.writeFileSync('src/contracts/MyToken.json', JSON.stringify(myToken.abi, null, 2));
"

node -e "
const fs = require('fs');
const myTokenSale = JSON.parse(fs.readFileSync('../out/MyTokenSale.sol/MyTokenSale.json', 'utf8'));
fs.writeFileSync('src/contracts/MyTokenSale.json', JSON.stringify(myTokenSale.abi, null, 2));
"

node -e "
const fs = require('fs');
const kycContract = JSON.parse(fs.readFileSync('../out/KycContract.sol/KycContract.json', 'utf8'));
fs.writeFileSync('src/contracts/KycContract.json', JSON.stringify(kycContract.abi, null, 2));
"

生成されるABIファイルの例(MyToken.json):

[
  {
    "type": "constructor",
    "inputs": [
      {
        "name": "initialSupply",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "balanceOf",
    "inputs": [
      {
        "name": "account",
        "type": "address",
        "internalType": "address"
      }
    ],
    "outputs": [
      {
        "name": "",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "view"
  },
  // ... その他の関数
]

4.2 ABIの型定義

TypeScriptでABIを使用する際の型定義:

// as const アサーションで型を保持
import MyTokenABI from './contracts/MyToken.json' assert { type: 'json' };

// または
const MyTokenABI = [...] as const;

// これにより、関数名や引数の型が推論される

5. Wagmi設定ファイルの作成

5.1 wagmi.tsの実装

src/wagmi.tsの作成:

import { http, createConfig } from 'wagmi';
import { localhost } from 'wagmi/chains';
import { injected } from 'wagmi/connectors';

export const config = createConfig({
  chains: [localhost],
  connectors: [injected()],
  transports: {
    [localhost.id]: http(),
  },
});

設定の詳細解説:

1. chains設定:

import { localhost, mainnet, sepolia } from 'wagmi/chains';

// ローカル開発用
chains: [localhost]

// 複数チェーン対応
chains: [mainnet, sepolia, localhost]

利用可能なチェーン:

  • mainnet: Ethereumメインネット
  • sepolia: Sepoliaテストネット
  • goerli: Goerliテストネット(非推奨)
  • polygon: Polygon
  • arbitrum: Arbitrum
  • optimism: Optimism
  • localhost: ローカルノード(Anvil、Hardhat、Ganache)

2. connectors設定:

import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';

// MetaMaskなどのブラウザウォレット
connectors: [injected()]

// 複数のコネクター
connectors: [
  injected(),
  walletConnect({ projectId: 'YOUR_PROJECT_ID' }),
  coinbaseWallet({ appName: 'My App' })
]

3. transports設定:

// HTTPトランスポート(デフォルト)
transports: {
  [localhost.id]: http(),
  [mainnet.id]: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
}

// WebSocketトランスポート(リアルタイム更新)
import { webSocket } from 'wagmi';

transports: {
  [mainnet.id]: webSocket('wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
}

5.2 コントラクト設定ファイルの作成

src/config/contracts.tsの作成:

import MyTokenABI from '../contracts/MyToken.json';
import MyTokenSaleABI from '../contracts/MyTokenSale.json';
import KycContractABI from '../contracts/KycContract.json';

// Contract addresses - update these with your deployed contract addresses
export const contractAddresses = {
  // Anvil local testnet (chainId: 31337)
  31337: {
    myToken: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
    myTokenSale: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
    kycContract: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0',
  },
  // Add other networks as needed
} as const;

export const contracts = {
  myToken: {
    abi: MyTokenABI,
  },
  myTokenSale: {
    abi: MyTokenSaleABI,
  },
  kycContract: {
    abi: KycContractABI,
  },
} as const;

設定の詳細解説:

1. contractAddresses:

// チェーンIDごとにアドレスを管理
export const contractAddresses = {
  31337: { // Anvil/Hardhat
    myToken: '0x...',
  },
  1: { // Mainnet
    myToken: '0x...',
  },
  11155111: { // Sepolia
    myToken: '0x...',
  },
} as const;

// 使用例
const chainId = chain?.id || 31337;
const addresses = contractAddresses[chainId];

2. contracts:

// ABIを一元管理
export const contracts = {
  myToken: {
    abi: MyTokenABI,
  },
} as const;

// 使用例
import { contracts } from './config/contracts';

const { data } = useReadContract({
  address: '0x...',
  abi: contracts.myToken.abi,
  functionName: 'balanceOf',
  args: [address],
});

as constの重要性:

// ❌ as constなし
export const contracts = {
  myToken: { abi: MyTokenABI }
};
// 型: { myToken: { abi: any[] } }

// ✅ as constあり
export const contracts = {
  myToken: { abi: MyTokenABI }
} as const;
// 型: { readonly myToken: { readonly abi: readonly [...] } }
// ABIの詳細な型情報が保持される

6. プロバイダーの設定

6.1 main.tsxの更新

src/main.tsxの実装:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import './index.css'
import App from './App.tsx'
import { config } from './wagmi'

const queryClient = new QueryClient()

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </WagmiProvider>
  </StrictMode>,
)

プロバイダーの階層構造:

StrictMode (React開発モードでの追加チェック)

WagmiProvider (Web3の状態管理)

QueryClientProvider (データキャッシング)

App (アプリケーション本体)

各プロバイダーの役割:

1. StrictMode:

// Reactの潜在的な問題を検出
<StrictMode>
  <App />
</StrictMode>

// 開発モードでのみ動作
// - 非推奨APIの警告
// - 副作用の二重実行(バグ検出用)

2. WagmiProvider:

<WagmiProvider config={config}>
  {/* Web3の状態がこの配下で利用可能 */}
  <App />
</WagmiProvider>

// 提供される機能:
// - アカウント情報
// - コントラクトの読み書き
// - トランザクションの状態

3. QueryClientProvider:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false, // ウィンドウフォーカス時の再取得を無効化
      retry: 3, // 失敗時のリトライ回数
    },
  },
});

<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

// 提供される機能:
// - データキャッシング
// - 自動再取得
// - ローディング状態管理

7. メインアプリケーションの実装

7.1 App.tsxの全体像

src/App.tsxの実装:

import { useState, useEffect } from 'react';
import {
  useAccount,
  useConnect,
  useDisconnect,
  useReadContract,
  useWriteContract,
  useWatchContractEvent
} from 'wagmi';
import { parseEther } from 'viem';
import { contracts, contractAddresses } from './config/contracts';
import './App.css';

function App() {
  const [kycAddress, setKycAddress] = useState('0x123...');
  const [userTokens, setUserTokens] = useState<bigint>(0n);

  const { address, isConnected, chain } = useAccount();
  const { connect, connectors } = useConnect();
  const { disconnect } = useDisconnect();
  const { writeContract } = useWriteContract();

  // Get contract addresses for current chain
  const chainId = chain?.id || 31337;
  const addresses = contractAddresses[chainId as keyof typeof contractAddresses];

  // Read user token balance
  const { data: balance, refetch: refetchBalance } = useReadContract({
    address: addresses?.myToken as `0x${string}`,
    abi: contracts.myToken.abi,
    functionName: 'balanceOf',
    args: address ? [address] : undefined,
  });

  // Update userTokens when balance changes
  useEffect(() => {
    if (balance !== undefined) {
      setUserTokens(balance as bigint);
    }
  }, [balance]);

  // Watch for Transfer events to this address
  useWatchContractEvent({
    address: addresses?.myToken as `0x${string}`,
    abi: contracts.myToken.abi,
    eventName: 'Transfer',
    args: address ? { to: address } : undefined,
    onLogs: () => {
      refetchBalance();
    },
  });

  // Handle KYC whitelisting
  const handleKycWhitelisting = async () => {
    if (!addresses) return;

    try {
      await writeContract({
        address: addresses.kycContract as `0x${string}`,
        abi: contracts.kycContract.abi,
        functionName: 'setKycCompleted',
        args: [kycAddress as `0x${string}`],
      });
      alert(`KYC for ${kycAddress} is completed`);
    } catch (error) {
      console.error('KYC whitelisting failed:', error);
      alert('KYC whitelisting failed. See console for details.');
    }
  };

  // Handle buying tokens
  const handleBuyTokens = async () => {
    if (!addresses || !address) return;

    try {
      await writeContract({
        address: addresses.myTokenSale as `0x${string}`,
        abi: contracts.myTokenSale.abi,
        functionName: 'buyTokens',
        args: [address],
        value: parseEther('0.000000000000000001'), // 1 wei
      });
    } catch (error) {
      console.error('Token purchase failed:', error);
      alert('Token purchase failed. See console for details.');
    }
  };

  if (!isConnected) {
    return (
      <div className="App">
        <h1>StarDucks Cappucino Token Sale</h1>
        <p>Please connect your wallet to continue</p>
        <div>
          {connectors.map((connector) => (
            <button
              key={connector.id}
              onClick={() => connect({ connector })}
              type="button"
            >
              Connect {connector.name}
            </button>
          ))}
        </div>
      </div>
    );
  }

  return (
    <div className="App">
      <h1>StarDucks Cappucino Token Sale</h1>
      <div style={{ marginBottom: '20px' }}>
        <p>Connected: {address}</p>
        <button onClick={() => disconnect()} type="button">
          Disconnect
        </button>
      </div>

      <p>Get your Tokens today!</p>

      <h2>KYC Whitelisting</h2>
      <div>
        <label>
          Address to allow:{' '}
          <input
            type="text"
            name="kycAddress"
            value={kycAddress}
            onChange={(e) => setKycAddress(e.target.value)}
          />
        </label>
        <button type="button" onClick={handleKycWhitelisting}>
          Add to Whitelist
        </button>
      </div>

      <h2>Buy Tokens</h2>
      <p>
        If you want to buy tokens, send Wei to this address:{' '}
        {addresses?.myTokenSale}
      </p>
      <p>You currently have: {userTokens.toString()} CAPPU Tokens</p>
      <button type="button" onClick={handleBuyTokens}>
        Buy more tokens
      </button>
    </div>
  );
}

export default App;

7.2 コードの詳細解説

ステート管理

const [kycAddress, setKycAddress] = useState('0x123...');
const [userTokens, setUserTokens] = useState<bigint>(0n);

useState Hook:

  • kycAddress: KYC承認するアドレス(ユーザー入力)
  • userTokens: ユーザーのトークン残高(BigInt型)

BigInt型の使用:

// ❌ 間違い: numberは安全に扱える範囲が限定的
const tokens: number = 1000000000000000000;

// ✅ 正しい: BigIntで大きな数値を正確に扱う
const tokens: bigint = 1000000000000000000n;

// BigIntの演算
const double = tokens * 2n; // BigIntの計算はn suffix必要
const string = tokens.toString(); // 文字列に変換

Wagmi Hooksの使用

1. useAccount - アカウント情報の取得:

const { address, isConnected, chain } = useAccount();

// address: 接続されたウォレットのアドレス(0x...)
// isConnected: 接続状態のboolean
// chain: 接続中のチェーン情報

使用例:

if (!isConnected) {
  return <div>Please connect wallet</div>;
}

return (
  <div>
    <p>Address: {address}</p>
    <p>Chain: {chain?.name} (ID: {chain?.id})</p>
  </div>
);

2. useConnect - ウォレット接続:

const { connect, connectors, isPending } = useConnect();

// connect: ウォレット接続関数
// connectors: 利用可能なコネクターのリスト
// isPending: 接続処理中かどうか

使用例:

{connectors.map((connector) => (
  <button
    key={connector.id}
    onClick={() => connect({ connector })}
    disabled={isPending}
  >
    {isPending ? 'Connecting...' : `Connect ${connector.name}`}
  </button>
))}

3. useReadContract - コントラクト読み取り:

const { data: balance, isLoading, refetch } = useReadContract({
  address: addresses?.myToken as `0x${string}`,
  abi: contracts.myToken.abi,
  functionName: 'balanceOf',
  args: address ? [address] : undefined,
});

// data: 読み取った値
// isLoading: ローディング状態
// refetch: 再取得関数

キャッシュと再取得:

// 自動キャッシュされる
const { data } = useReadContract({...});

// 手動で再取得
const { refetch } = useReadContract({...});
await refetch();

// 定期的に再取得
const { data } = useReadContract({
  // ...
  query: {
    refetchInterval: 10000, // 10秒ごと
  },
});

4. useWriteContract - コントラクト書き込み:

const { writeContract, isPending, isSuccess } = useWriteContract();

// writeContract: トランザクション送信関数
// isPending: トランザクション処理中
// isSuccess: トランザクション成功

使用例:

const handleBuy = async () => {
  try {
    await writeContract({
      address: '0x...',
      abi: contractAbi,
      functionName: 'buyTokens',
      args: [userAddress],
      value: parseEther('1'), // 1 ETH送信
    });
  } catch (error) {
    console.error('Transaction failed:', error);
  }
};

5. useWatchContractEvent - イベント監視:

useWatchContractEvent({
  address: addresses?.myToken as `0x${string}`,
  abi: contracts.myToken.abi,
  eventName: 'Transfer',
  args: address ? { to: address } : undefined,
  onLogs: (logs) => {
    console.log('Transfer event detected:', logs);
    refetchBalance(); // 残高を再取得
  },
});

イベントフィルタリング:

// 特定のアドレス宛てのTransferのみ監視
useWatchContractEvent({
  eventName: 'Transfer',
  args: { to: myAddress }, // toが一致するイベントのみ
  onLogs: (logs) => {
    // このアドレス宛てのTransferのみ
  },
});

// 複数の条件
useWatchContractEvent({
  eventName: 'Transfer',
  args: {
    from: senderAddress,
    to: receiverAddress,
  },
  onLogs: (logs) => {
    // fromとtoが両方一致するイベント
  },
});

コントラクト関数の呼び出し

KYCホワイトリストへの追加:

const handleKycWhitelisting = async () => {
  if (!addresses) return;

  try {
    await writeContract({
      address: addresses.kycContract as `0x${string}`,
      abi: contracts.kycContract.abi,
      functionName: 'setKycCompleted',
      args: [kycAddress as `0x${string}`],
    });
    alert(`KYC for ${kycAddress} is completed`);
  } catch (error) {
    console.error('KYC whitelisting failed:', error);
    alert('KYC whitelisting failed. See console for details.');
  }
};

処理フロー:

  1. アドレスの存在確認
  2. writeContract呼び出し(トランザクション送信)
  3. ユーザーがウォレットで承認
  4. トランザクションが確認される
  5. 成功メッセージ表示

トークン購入:

const handleBuyTokens = async () => {
  if (!addresses || !address) return;

  try {
    await writeContract({
      address: addresses.myTokenSale as `0x${string}`,
      abi: contracts.myTokenSale.abi,
      functionName: 'buyTokens',
      args: [address],
      value: parseEther('0.000000000000000001'), // 1 wei
    });
  } catch (error) {
    console.error('Token purchase failed:', error);
    alert('Token purchase failed. See console for details.');
  }
};

valueの指定:

// parseEtherを使用してETHをweiに変換
value: parseEther('1')      // 1 ETH
value: parseEther('0.1')    // 0.1 ETH
value: parseEther('0.000000000000000001') // 1 wei

// 直接BigIntで指定
value: 1n // 1 wei
value: 1000000000000000000n // 1 ETH (18桁)

条件分岐レンダリング

接続前の画面:

if (!isConnected) {
  return (
    <div className="App">
      <h1>StarDucks Cappucino Token Sale</h1>
      <p>Please connect your wallet to continue</p>
      <div>
        {connectors.map((connector) => (
          <button
            key={connector.id}
            onClick={() => connect({ connector })}
            type="button"
          >
            Connect {connector.name}
          </button>
        ))}
      </div>
    </div>
  );
}

Reactの条件分岐パターン:

// パターン1: Early return
if (!isConnected) {
  return <ConnectWallet />;
}
return <MainApp />;

// パターン2: 三項演算子
return isConnected ? <MainApp /> : <ConnectWallet />;

// パターン3: 論理AND演算子
return (
  <>
    {!isConnected && <ConnectWallet />}
    {isConnected && <MainApp />}
  </>
);

8. ビルドとテスト

8.1 開発サーバーの起動

開発モードで実行:

npm run dev

出力例:

  VITE v7.2.4  ready in 342 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

開発サーバーの特徴:

  • 高速なHMR: ファイル変更が即座に反映
  • エラー表示: ブラウザにエラーオーバーレイ表示
  • TypeScriptチェック: 型エラーをリアルタイム検出

開発ワークフロー:

  1. npm run devでサーバー起動
  2. ブラウザでhttp://localhost:5173/を開く
  3. コードを編集
  4. ブラウザが自動更新(HMR)

8.2 TypeScriptのコンパイル

型チェックの実行:

# TypeScriptコンパイラで型チェック
npx tsc --noEmit

# または、ビルドスクリプトに含まれている
npm run build

型エラーの例:

// ❌ 型エラー
const balance: number = await contract.balanceOf(address);
// Error: Type 'bigint' is not assignable to type 'number'

// ✅ 修正
const balance: bigint = await contract.balanceOf(address);

tsconfig.jsonの主要設定:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

8.3 プロダクションビルド

ビルドの実行:

npm run build

ビルドプロセス:

  1. TypeScriptコンパイル(型チェック)
  2. Viteによるバンドル(Rollup)
  3. 最適化(minify、tree-shaking)
  4. 静的アセットの処理

ビルド出力例:

> frontend@0.0.0 build
> tsc -b && vite build

vite v7.3.0 building client environment for production...
✓ 1455 modules transformed.
dist/index.html                      0.46 kB │ gzip:   0.29 kB
dist/assets/index-COcDBgFa.css       1.38 kB │ gzip:   0.70 kB
dist/assets/ccip-B4MYfB3J.js         4.28 kB │ gzip:   1.87 kB
dist/assets/secp256k1-BSR98dvY.js   27.38 kB │ gzip:  10.73 kB
dist/assets/index-Cf4fRyaE.js      416.06 kB │ gzip: 126.66 kB
✓ built in 2.35s

生成されるdistディレクトリ:

dist/
├── index.html
└── assets/
    ├── index-Cf4fRyaE.js       # メインJavaScript
    ├── index-COcDBgFa.css      # スタイルシート
    └── ...                     # その他のアセット

ビルドの最適化:

  • Code Splitting: ページごとにファイル分割
  • Tree Shaking: 未使用コードの削除
  • Minification: コードの圧縮
  • Gzip Compression: gzipサイズの計算

8.4 ビルド結果のプレビュー

プレビューサーバーの起動:

npm run preview

出力例:

  ➜  Local:   http://localhost:4173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

プレビューの目的:

  • プロダクションビルドの動作確認
  • バンドルサイズの確認
  • パフォーマンステスト

8.5 Vitestによるテスト

テストの実行:

# 全テストを実行
npm run test

# ウォッチモード(変更を監視)
npm run test -- --watch

# カバレッジ計測
npm run test -- --coverage

# UIモード(ブラウザでテスト結果表示)
npm run test -- --ui

テストファイルの配置:

src/
├── App.tsx
├── App.test.tsx        # Appのテスト
├── components/
│   ├── Button.tsx
│   └── Button.test.tsx # Buttonのテスト
└── utils/
    ├── helpers.ts
    └── helpers.test.ts # helpersのテスト

サンプルテスト:

// App.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {
  it('renders title', () => {
    render(<App />);
    expect(screen.getByText('StarDucks Cappucino Token Sale')).toBeInTheDocument();
  });
});

ウォッチモードの利点:

npm run test -- --watch

# ファイルを編集すると自動的に関連テストが実行される
# 高速なフィードバックループ

9. 完成したプロジェクト構造

9.1 ディレクトリ構造

frontend/
├── node_modules/           # 依存パッケージ
├── public/                 # 静的アセット
│   └── vite.svg
├── src/
│   ├── assets/            # 画像などのアセット
│   │   └── react.svg
│   ├── config/            # 設定ファイル
│   │   └── contracts.ts   # コントラクトABIとアドレス
│   ├── contracts/         # ABIファイル
│   │   ├── MyToken.json
│   │   ├── MyTokenSale.json
│   │   └── KycContract.json
│   ├── App.css            # アプリケーションスタイル
│   ├── App.tsx            # メインコンポーネント
│   ├── index.css          # グローバルスタイル
│   ├── main.tsx           # エントリーポイント
│   └── wagmi.ts           # Wagmi設定
├── .gitignore
├── index.html             # HTMLテンプレート
├── package.json           # パッケージ定義
├── package-lock.json      # 依存関係ロック
├── tsconfig.json          # TypeScript設定
├── tsconfig.node.json     # Node用TypeScript設定
└── vite.config.ts         # Vite設定

9.2 主要ファイルの役割

ファイル 役割
src/main.tsx アプリケーションのエントリーポイント、プロバイダー設定
src/App.tsx メインアプリケーションロジック、UI
src/wagmi.ts Wagmi設定(チェーン、コネクター、トランスポート)
src/config/contracts.ts コントラクトABIとアドレスの管理
src/contracts/*.json Solidityコントラクトから抽出したABI
vite.config.ts Viteのビルド設定、テスト設定
tsconfig.json TypeScriptコンパイラ設定
package.json 依存関係とスクリプト定義

9.3 実装された機能

1. ウォレット接続:

  • MetaMaskなどのブラウザウォレットとの接続
  • 接続状態の表示
  • 切断機能

2. トークン残高表示:

  • 接続したアドレスのトークン残高を表示
  • Transferイベントを監視して自動更新
  • BigInt型での正確な表示

3. KYCホワイトリスト:

  • アドレスをKYC承認リストに追加
  • オーナーのみが実行可能(コントラクト側で制御)
  • トランザクション結果の通知

4. トークン購入:

  • ETH(wei)を送信してトークンを購入
  • KYC承認済みアドレスのみ購入可能
  • エラーハンドリング

5. リアルタイム更新:

  • Transferイベントの監視
  • 残高の自動再取得
  • ブロックチェーンとの同期

10. AppSample.jsとの比較

10.1 技術スタックの違い

項目 AppSample.js(旧) App.tsx(新)
言語 JavaScript TypeScript
ライブラリ Web3.js Viem + Wagmi
フレームワーク React Class React Hooks
ビルドツール Webpack Vite
型安全性 なし あり

10.2 コードの比較

クラスコンポーネント vs 関数コンポーネント:

// AppSample.js - クラスコンポーネント
class App extends Component {
  state = { loaded: false, userTokens: 0 };

  componentDidMount = async () => {
    this.web3 = await getWeb3();
    this.accounts = await this.web3.eth.getAccounts();
    // ...
  }

  render() {
    return <div>...</div>;
  }
}
// App.tsx - 関数コンポーネント + Hooks
function App() {
  const [userTokens, setUserTokens] = useState<bigint>(0n);
  const { address, isConnected } = useAccount();

  useEffect(() => {
    // ...
  }, []);

  return <div>...</div>;
}

Web3.js vs Viem + Wagmi:

// AppSample.js - Web3.js
this.tokenInstance = new this.web3.eth.Contract(
  MyToken.abi,
  MyToken.networks[this.networkId].address
);

let userTokens = await this.tokenInstance.methods
  .balanceOf(this.accounts[0])
  .call();
// App.tsx - Viem + Wagmi
const { data: balance } = useReadContract({
  address: addresses.myToken,
  abi: contracts.myToken.abi,
  functionName: 'balanceOf',
  args: [address],
});

イベントリスニング:

// AppSample.js
listenToTokenTransfer = () => {
  this.tokenInstance.events.Transfer({to: this.accounts[0]})
    .on("data", this.updateUserTokens);
}
// App.tsx
useWatchContractEvent({
  address: addresses.myToken,
  abi: contracts.myToken.abi,
  eventName: 'Transfer',
  args: { to: address },
  onLogs: () => refetchBalance(),
});

10.3 新しいスタックの利点

1. 型安全性:

// ✅ TypeScript: コンパイル時にエラー検出
const balance: bigint = await readContract(...);
const invalid: number = balance; // エラー: bigintをnumberに代入不可

// ❌ JavaScript: 実行時にエラー
const balance = await contract.methods.balanceOf(...).call();
const doubled = balance * 2; // 動作するが期待通りでない可能性

2. 優れた開発体験:

// Wagmiは自動補完が効く
const { data } = useReadContract({
  functionName: 'balance', // タイポ → エラー
  functionName: 'balanceOf', // ✅ 正しい(自動補完)
});

3. パフォーマンス:

  • Vite: 高速な起動とHMR
  • Viem: 小さいバンドルサイズ
  • キャッシング: 自動的なデータキャッシュ

4. モダンなパターン:

  • React Hooks: 関数コンポーネント
  • 宣言的なAPI: useReadContract、useWriteContract
  • 自動状態管理: ローディング、エラー、成功

11. デプロイの準備

11.1 コントラクトアドレスの設定

デプロイ後の設定:

// src/config/contracts.ts
export const contractAddresses = {
  // ローカル開発(Anvil)
  31337: {
    myToken: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
    myTokenSale: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
    kycContract: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0',
  },
  // Sepoliaテストネット
  11155111: {
    myToken: '0x...', // デプロイ後のアドレスを設定
    myTokenSale: '0x...',
    kycContract: '0x...',
  },
  // メインネット
  1: {
    myToken: '0x...',
    myTokenSale: '0x...',
    kycContract: '0x...',
  },
} as const;

11.2 環境変数の設定

.env.localの作成:

# フロントエンド用の環境変数
VITE_ALCHEMY_KEY=your_alchemy_key
VITE_WALLETCONNECT_PROJECT_ID=your_project_id

環境変数の使用:

// src/wagmi.ts
import { http } from 'wagmi';
import { mainnet, sepolia } from 'wagmi/chains';

export const config = createConfig({
  chains: [mainnet, sepolia],
  transports: {
    [mainnet.id]: http(`https://eth-mainnet.g.alchemy.com/v2/${import.meta.env.VITE_ALCHEMY_KEY}`),
    [sepolia.id]: http(`https://eth-sepolia.g.alchemy.com/v2/${import.meta.env.VITE_ALCHEMY_KEY}`),
  },
});

11.3 静的サイトホスティング

Vercelへのデプロイ:

# Vercel CLIのインストール
npm i -g vercel

# デプロイ
vercel --prod

Netlifyへのデプロイ:

# Netlify CLIのインストール
npm i -g netlify-cli

# ビルド
npm run build

# デプロイ
netlify deploy --prod --dir=dist

GitHub Pagesへのデプロイ:

# vite.config.tsにbase設定を追加
export default defineConfig({
  base: '/repository-name/',
  // ...
});

# gh-pagesパッケージをインストール
npm install -D gh-pages

# package.jsonにスクリプト追加
{
  "scripts": {
    "deploy": "npm run build && gh-pages -d dist"
  }
}

# デプロイ
npm run deploy

12. トラブルシューティング

12.1 よくある問題と解決方法

問題1: ウォレット接続できない

// エラー: No connector provided

// 原因: WagmiProviderが設定されていない
// 解決: main.tsxでWagmiProviderを確認

// ❌ 間違い
<App />

// ✅ 正しい
<WagmiProvider config={config}>
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
</WagmiProvider>

問題2: 型エラー

// エラー: Type 'string | undefined' is not assignable to type '0x${string}'

// 原因: undefinedの可能性がある値を型アサーションなしで使用

// ❌ 間違い
<div>{addresses.myToken}</div>

// ✅ 正しい
<div>{addresses?.myToken}</div>

問題3: BigInt表示エラー

// エラー: Do not know how to serialize a BigInt

// 原因: BigIntを直接JSXに渡している

// ❌ 間違い
<div>Balance: {balance}</div>

// ✅ 正しい
<div>Balance: {balance?.toString()}</div>

問題4: CORS エラー

// エラー: Access to fetch at ... has been blocked by CORS policy

// 原因: ローカルノード(Anvil)がCORSを許可していない

// 解決: Anvilを--hostオプション付きで起動
// anvil --host 0.0.0.0

問題5: Contract ABIエラー

// エラー: ABI is not properly formatted

// 原因: ABIが配列ではなくオブジェクト全体を読み込んでいる

// ❌ 間違い
const abi = MyTokenArtifact; // { abi: [...], bytecode: ... }

// ✅ 正しい
const abi = MyTokenArtifact.abi; // [...]

12.2 デバッグのヒント

ブラウザの開発者ツール:

// Console
console.log('Balance:', balance?.toString());
console.log('Address:', address);

// Networkタブでトランザクションを確認
// JSON-RPC呼び出しを確認

React Developer Tools:

// Chromeエクステンションをインストール
// コンポーネントツリーを確認
// Hooksの状態を確認

Wagmi DevTools:

// 開発環境でのデバッグツール
import { WagmiDevTools } from 'wagmi';

<WagmiProvider config={config}>
  <App />
  <WagmiDevTools />
</WagmiProvider>

13. まとめ

13.1 実装内容

1月5日に構築したフロントエンド:

  1. ✅ Vite + React + TypeScriptのプロジェクトセットアップ
  2. ✅ Viem + Wagmiの統合
  3. ✅ Vitestのテスト環境構築
  4. ✅ コントラクトABIの抽出と設定
  5. ✅ ウォレット接続機能
  6. ✅ トークン残高表示
  7. ✅ KYCホワイトリスト機能
  8. ✅ トークン購入機能
  9. ✅ イベントリスニング

13.2 技術スタック

使用技術:

  • Vite 7.2.4: 高速ビルドツール
  • React 19.2.0: UIライブラリ
  • TypeScript 5.9.3: 型安全な開発
  • Viem 2.43.5: モダンなEthereumライブラリ
  • Wagmi 3.1.4: React Web3フック
  • Vitest 4.0.16: テストフレームワーク
  • TanStack Query 5.90.16: データフェッチング

13.3 学んだこと

モダンWeb3開発のベストプラクティス:

  1. 型安全性: TypeScriptによるコンパイル時エラー検出
  2. 宣言的API: React Hooksでシンプルなコード
  3. パフォーマンス: Viteの高速開発サーバー
  4. キャッシング: 自動的なデータキャッシュとリフレッチ
  5. エラーハンドリング: try-catchと型チェック
  6. モジュラー設計: 設定ファイルの分離

旧スタックからの主な改善点:

  • Web3.js → Viem: 軽量化と型安全性
  • Class Component → Hooks: シンプルで読みやすい
  • Webpack → Vite: 高速な開発体験
  • JavaScript → TypeScript: バグの早期発見

13.4 次のステップ

今後の拡張案:

  1. ユーザー体験の向上:

    • トランザクション待機中のローディング表示
    • エラーメッセージの改善
    • トースト通知の追加
  2. 機能追加:

    • トークンセールのフェーズ表示
    • 購入履歴の表示
    • マルチチェーン対応
  3. テストの充実:

    • コンポーネントテストの追加
    • E2Eテスト(Playwright/Cypress)
    • モックによるコントラクト呼び出しのテスト
  4. デプロイ:

    • Vercel/Netlifyへのデプロイ
    • CI/CDパイプラインの構築
    • 環境変数の管理
  5. 最適化:

    • Code splittingによるバンドルサイズ削減
    • 画像最適化
    • PWA化

このフロントエンド実装により、モダンなWeb3開発の基礎を習得しました。型安全で保守性の高い、ユーザーフレンドリーなDAppの構築が可能になりました。

Discussion