既存Webスキルで始める!ReactとEthers.jsによるWeb3フロントエンド開発実践ガイド
Web2の世界で培ったフロントエンド開発のスキルは、Web3の分散型アプリケーション(dApp)開発においても非常に強力な武器となります。特に、モダンなWeb開発のデファクトスタンダードであるReactと、Ethereumブロックチェーンとのインタラクションを担う軽量なライブラリEthers.jsの組み合わせは、Web3フロントエンド開発の主流の一つです。
この記事では、Webエンジニアの皆さんが自身の既存スキルを活かし、ReactとEthers.jsを用いてWeb3フロントエンド開発を始めるための実践的なガイドを提供します。基本的な概念から、具体的なコード例を交えた開発手法、そして開発におけるベストプラクティスまでを網羅的に解説いたします。
1. Web2とWeb3フロントエンド開発のパラダイムシフト
従来のWeb2アプリケーションは、ユーザーのデータやビジネスロジックが中央集権型のサーバーに集中していました。これに対し、Web3アプリケーションは、スマートコントラクトやブロックチェーン、IPFSといった分散型技術を基盤とします。フロントエンド開発の観点から見ると、この変化は主にデータの取得とトランザクションの実行方法に現れます。
- Web2: REST APIやGraphQLを通じてサーバーからデータを取得し、HTTPリクエストでデータを送信します。認証はID/パスワードやOAuthが主流です。
- Web3: ブロックチェーンネットワークに直接接続し、スマートコントラクトの公開関数を呼び出してデータを読み書きします。ユーザー認証は、MetaMaskなどのウォレットを介した署名(Signer)によって行われます。
この根本的な違いを理解することが、Web3フロントエンド開発の第一歩です。既存のReact開発スキルは、UI/UXの構築や状態管理の面でそのまま活かせますが、ブロックチェーンとの連携部分に新たな知識が必要になります。
2. Ethers.jsの基礎:ブロックチェーンとの対話
Ethers.jsは、JavaScript/TypeScriptからEthereumブロックチェーンとインタラクションするための強力なライブラリです。Web3.jsと比較してより軽量で、TypeScriptフレンドリーな設計が特徴です。Ethers.jsを使いこなす上で重要な3つの概念を解説します。
2.1. Provider(プロバイダー)
Providerは、ブロックチェーンネットワークへの読み取り専用接続を提供します。これにより、アカウントの残高取得、トランザクションの取得、スマートコントラクトのView関数(状態を変更しない読み取り専用関数)の呼び出しなどが可能になります。
import { ethers } from "ethers";
// MetaMaskが注入するwindow.ethereumを使用する例
if (window.ethereum) {
const provider = new ethers.BrowserProvider(window.ethereum);
provider.getBalance("0xYourWalletAddress").then((balance) => {
console.log(`Balance: ${ethers.formatEther(balance)} ETH`);
});
}
ethers.BrowserProvider
は、MetaMaskのようなブラウザ拡張ウォレットが提供する window.ethereum
オブジェクトをラップする際に便利です。
2.2. Signer(サイナー)
Signerは、ウォレットのアカウントを表現し、トランザクションの署名やスマートコントラクトのState変更関数(状態を変更する書き込み関数)の実行を可能にします。MetaMaskなどのウォレットを通じてユーザーの承認を得る必要があります。
import { ethers } from "ethers";
async function connectWalletAndGetSigner() {
if (window.ethereum) {
const provider = new ethers.BrowserProvider(window.ethereum);
// ユーザーにMetaMaskへの接続を要求
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
console.log(`Connected account: ${await signer.getAddress()}`);
return signer;
}
return null;
}
2.3. Contract(コントラクト)
Contractクラスは、特定のスマートコントラクトとインタラクトするための抽象化を提供します。コントラクトのアドレスとABI(Application Binary Interface)を基にインスタンス化し、定義された関数を呼び出すことができます。
import { ethers } from "ethers";
// サンプルコントラクトのABIとアドレス
const contractAddress = "0xYourContractAddress";
const contractABI = [
// ... コントラクトのABI配列 ...
"function name() view returns (string)",
"function setValue(string _newValue) public",
"event ValueChanged(string oldValue, string newValue)"
];
async function interactWithContract() {
const signer = await connectWalletAndGetSigner();
if (!signer) return;
const contract = new ethers.Contract(contractAddress, contractABI, signer);
// View関数(読み取り専用)の呼び出し
const name = await contract.name();
console.log(`Contract Name: ${name}`);
// State変更関数(書き込み)の呼び出し
try {
const tx = await contract.setValue("Hello Web3!");
await tx.wait(); // トランザクションがマイニングされるまで待機
console.log("Value updated successfully!");
} catch (error) {
console.error("Error setting value:", error);
}
}
3. ReactとEthers.jsによるWeb3フロントエンドの構築
Reactアプリケーション内でEthers.jsを効果的に統合するためには、非同期処理と状態管理が鍵となります。
3.1. ウォレット接続コンポーネントの作成
ユーザーがウォレットを接続し、アカウント情報を表示する基本的なコンポーネントを実装します。
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
const WalletConnector: React.FC = () => {
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [balance, setBalance] = useState<string | null>(null);
useEffect(() => {
// ページロード時にウォレット接続状態を確認(任意)
if (window.ethereum) {
const provider = new ethers.BrowserProvider(window.ethereum);
provider.listAccounts().then(accounts => {
if (accounts.length > 0) {
provider.getSigner().then(s => {
setSigner(s);
s.getAddress().then(a => setAddress(a));
s.getBalance().then(b => setBalance(ethers.formatEther(b)));
});
}
});
}
}, []);
const connectWallet = async () => {
if (!window.ethereum) {
alert("MetaMaskなどのEthereumウォレットをインストールしてください。");
return;
}
try {
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []); // 接続要求
const newSigner = await provider.getSigner();
setSigner(newSigner);
const newAddress = await newSigner.getAddress();
setAddress(newAddress);
const newBalance = await newSigner.getBalance();
setBalance(ethers.formatEther(newBalance));
} catch (error) {
console.error("ウォレット接続エラー:", error);
}
};
return (
<div>
{address ? (
<div>
<p>接続中のアドレス: {address}</p>
<p>残高: {balance} ETH</p>
</div>
) : (
<button onClick={connectWallet}>ウォレットを接続</button>
)}
</div>
);
};
export default WalletConnector;
3.2. スマートコントラクトとのインタラクション
ウォレットが接続されたら、そのSignerを使ってスマートコントラクトの関数を呼び出すことができます。
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
// App.tsxなど、上位コンポーネントからsignerをpropsで渡す想定
interface ContractInteractionProps {
signer: ethers.Signer | null;
}
const contractAddress = "0xYourContractAddress"; // 実際にデプロイされたコントラクトアドレス
const contractABI = [
"function currentMessage() view returns (string)",
"function setMessage(string _newMessage) public",
"event MessageUpdated(string oldMessage, string newMessage)"
];
const MessageBoard: React.FC<ContractInteractionProps> = ({ signer }) => {
const [message, setMessage] = useState<string>("");
const [newMessageInput, setNewMessageInput] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
// メッセージを読み込む関数
const fetchMessage = async () => {
if (!signer) return;
try {
const provider = signer.provider;
if (!provider) return;
const contract = new ethers.Contract(contractAddress, contractABI, provider);
const currentMsg = await contract.currentMessage();
setMessage(currentMsg);
} catch (error) {
console.error("メッセージの読み込みエラー:", error);
}
};
// メッセージを更新する関数
const updateMessage = async () => {
if (!signer) {
alert("ウォレットを接続してください。");
return;
}
if (!newMessageInput.trim()) {
alert("新しいメッセージを入力してください。");
return;
}
setLoading(true);
try {
const contract = new ethers.Contract(contractAddress, contractABI, signer);
const tx = await contract.setMessage(newMessageInput);
await tx.wait(); // トランザクションの完了を待つ
alert("メッセージが更新されました!");
setNewMessageInput("");
await fetchMessage(); // 更新後にメッセージを再読み込み
} catch (error) {
console.error("メッセージの更新エラー:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchMessage(); // コンポーネントロード時、またはsignerが変更された時にメッセージを読み込む
}, [signer]);
return (
<div>
<h3>メッセージボード</h3>
<p>現在のメッセージ: {message || "メッセージなし"}</p>
{signer ? (
<div>
<input
type="text"
value={newMessageInput}
onChange={(e) => setNewMessageInput(e.target.value)}
placeholder="新しいメッセージを入力"
disabled={loading}
/>
<button onClick={updateMessage} disabled={loading}>
{loading ? "送信中..." : "メッセージを更新"}
</button>
</div>
) : (
<p>ウォレットを接続してメッセージを更新できます。</p>
)}
</div>
);
};
export default MessageBoard;
上記は非常に基本的な例ですが、ethers.Contract
を使ってスマートコントラクトの関数を呼び出し、トランザクションの送信と待機を行うプロセスを示しています。
4. 開発におけるベストプラクティスと考慮事項
Web3フロントエンド開発には、従来のWeb開発とは異なる特有の考慮事項があります。
- エラーハンドリングの徹底: ブロックチェーンのトランザクションは失敗する可能性があります(ガス不足、コントラクトのエラー、ネットワーク混雑など)。ユーザーにわかりやすいエラーメッセージを表示し、適切なリカバリパスを提供することが重要です。
- トランザクションのUX: トランザクションの確認、署名、マイニングには時間がかかります。ユーザーに待機中であることを伝え、現在のステータス(保留中、成功、失敗)を明確に表示することで、ユーザー体験を向上させることができます。
- ガス代(Gas Fee)の表示:
ユーザーがトランザクションを送信する際にはガス代が発生します。推定ガス代を表示することで、ユーザーが意思決定しやすくなります。Ethers.jsは
provider.getFeeData()
などでガス情報にアクセスできます。 - イベントリスニング: スマートコントラクトから発行されるイベントをリッスンすることで、ブロックチェーンの状態変化をリアルタイムでフロントエンドに反映できます。これは効率的なUI更新に役立ちます。
- 開発環境の整備: HardhatやFoundryなどのローカル開発環境(テストネット)を構築し、スマートコントラクトのデプロイとテストを効率的に行うことが推奨されます。これにより、フロントエンドとスマートコントラクトの連携テストが容易になります。
- セキュリティ: ユーザーの秘密鍵を直接扱うことは絶対に避けてください。常にMetaMaskなどのウォレットを介してトランザクションを署名させる設計にしてください。また、従来のWebセキュリティ対策(XSS、CSRFなど)も引き続き重要です。
- 状態管理のパターン:
ethers.js
やウォレットの状態を一元的に管理するために、React Context APIやRedux、Zustandなどの状態管理ライブラリを活用することが効果的です。特にカスタムフックを作成し、Web3関連のロジックをカプセル化すると、コードの再利用性と保守性が向上します。
5. 次のステップと学習リソース
既存のWeb技術スキルを持つ皆さんは、Web3の世界への適応において非常に有利なスタート地点にいます。ReactとEthers.jsの基礎を習得したら、さらに深く学ぶためのリソースを活用してください。
- Ethers.js公式ドキュメント: https://docs.ethers.org/ 最も正確で詳細な情報源です。
- React公式ドキュメント: https://ja.react.dev/ Reactの最新のベストプラクティスを学ぶために不可欠です。
- Solidity公式ドキュメント: https://docs.soliditylang.org/ スマートコントラクトの理解を深めることで、より強固なフロントエンドを構築できます。
- Hardhat / Foundry: スマートコントラクト開発におけるデファクトスタンダードなツールです。ローカル環境でのテストとデプロイに役立ちます。
Web3の世界は急速に進化しています。新しい技術やツールが常に登場するため、最新の情報にアンテナを張り、積極的にコミュニティに参加することも重要です。
結論
ReactとEthers.jsの組み合わせは、既存のWeb開発スキルをWeb3の分散型アプリケーション開発に橋渡しするための強力なパスを提供します。この記事で紹介した基本的な開発手法とベストプラクティスを参考に、ぜひあなた自身のWeb3表現を形にしてみてください。技術的な挑戦は多いかもしれませんが、その分、新しい創造の可能性が広がっています。