#はじめに
[※注意!!]この記事は【初めてのDApps開発①】の続編です。まだ読んでいない人はそちらを読んでからこの記事を読んで下さい。
今回は最後の項目のReactを用いたフロントエンド開発を行います。
- Remixを用いたスマートコントラクト開発
- Truffleを用いたスマートコントラクトのデプロイ
- Reactを用いたフロントエンド開発
「Reactというフロントエンドフレームワークを使ってweb3.jsを経由してmetamaskを動かし, スマートコントラクトを動作させる」ということをやっていきます。
つまりこのようなイメージになります。
#作っていくアプリケーション
本記事では、【初めてのDApps開発①】でデプロイしたスマートコントラクトを使いながらアプリケーションの見た目の部分を作っていきます。
完成するアプリケーションは、以下のようになります。
URL: https://diarydapp-2a69c.web.app
では、一緒に作っていきましょう!!!
#セットアップ
まずディレクトリを作ります。
そこにweb3がすぐに使えるようになっているReact Truffle Boxというレポジトリをクローンします. npm install
を実行することで package.json
書かれている dependencies
をインストールしてくれます。
$ git clone https://github.com/truffle-box/react-box.git
$ cd react-box/client
$ npm install
私は【初めてのDApps開発①】で作成したdapps-deploy
ファイルと一まとめにして管理することにしました!
さて、【初めてのDApps開発①】で作成したdapps-deploy
ディレクトリ下の build/contract
に Resister.json
ファイルが生成されています。これはコントラクトのコンパイル時に生成されたものです。中身を丸ごとコピーしましょう。
そして react-box
ディレクトリのclient/src
下に Resister.json
というファイルを作ってそこに貼ります。
#Reactを記述していく!
まず、client/src
下の App.js
をこのように書き換えましょう。
import React from "react";
import Resister from "./Resister.json";
import getWeb3 from "./getWeb3";
import "./App.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
web3: null,
accounts: null,
contract: null,
name: null,
age: null,
hobby: null,
address: "",
outputName: null,
outputAge: null,
outputHobby: null,
};
}
componentDidMount = async () => {
try {
const web3 = await getWeb3();
const accounts = await web3.eth.getAccounts();
const networkId = await web3.eth.net.getId();
const deployedNetwork = Resister.networks[networkId];
const instance = new web3.eth.Contract(
Resister.abi,
deployedNetwork && deployedNetwork.address
);
this.setState({ web3, accounts, contract: instance });
} catch (error) {
alert(
`Failed to load web3, accounts, or contract. Check console for details.`
);
console.error(error);
}
const { accounts } = this.state;
console.log(accounts);
};
render() {
return (
<div className="App">
</div>
);
}
}
export default App;
componentDidMount
関数 は react のライフサイクルの一種でレンダリングがされた直後に実行される関数です。web3とコントラクトを使えるようにする処理を書いています。
render
はHTML的なものを書いていく所です。
とりあえず起動してみましょう。
$ npm start
まだ何も表示されないはずです。
###関数の作成
関数の作成をしていきます。
// アカウント情報の登録をする関数
writeRecord = async () => {
const { accounts, contract, name, age, hobby } = this.state;
const result = await contract.methods.registerAccount(name, age, hobby).send({
from: accounts[0],
});
console.log(result);
if (result.status === true) {
alert('会員登録が完了しました。');
}
};
コントラクトで定義した registerAccount 関数
を実行する関数です。ユーザーの name、age、hobby
という入力値を受け取って、引数に入れています。registerAccount 関数
はコントラクトのStorageにデータを書き込む関数なのでGas代を支払うアカウントを明記しています。ここでは自分のデータを書き込むユーザーが支払うように設定されています。
// アカウント情報を読み込む関数
viewRecord = async () => {
const { contract, accounts } = this.state;
console.log(contract);
const result = await contract.methods.viewAccount(accounts[0]).call();
console.log(result);
const outputName = result[0];
const outputAge = result[1];
const outputHobby = result[2];
this.setState({ outputName, outputAge, outputHobby });
};
コントラクトで定義した viewAccount 関数
を実行する関数です。
registerAccount 関数
と違うところはGas代を支払うユーザーを明記していない点です。viewAccount 関数
はStorageにデータを書き込まない(見るだけ)なのでGas代を支払う必要がありません。また受け取った結果を表示するためにstateに入れています。
handleChange = (name) => (event) => {
this.setState({ [name]: event.target.value });
};
ユーザーの入力値を受け取ってstateに入れる関数です。
以上で関数の作成は完了です。UIの作成に移ります。
###UIの作成
ReactだけでシンプルなUIを作っていきます!
render() {
return (
<div className="App">
<br />
<form>
<div>
<label>氏名:</label>
<input
onChange={this.handleChange("name")} />
</div>
<div>
<label>年齢:</label>
<input
onChange={this.handleChange("age")} />
</div>
<div>
<label>趣味:</label>
<input
onChange={this.handleChange("hobby")} />
</div>
<button type='button' onClick={this.writeRecord}>
会員登録
</button>
</form>
<br />
<br />
<form>
<label>検索したいアドレスを入力してください。</label>
<input onChange={this.handleChange("address")} />
<button type='button' onClick={this.viewRecord}>
検索
</button>
</form>
<br />
<br />
{this.state.outputName ? <p>氏名: {this.state.outputName}</p> : <p></p>}
{this.state.outputAge ? <p>年齢: {this.state.outputAge}</p> : <p></p>}
{this.state.outputHobby ? <p>趣味: {this.state.outputHobby}</p> : <p></p>}
</div>
);
}
以上の関数、UIの記述を行った後のApp.jsがこちらです。↓
import React from "react";
import Resister from "./Resister.json";
import getWeb3 from "./getWeb3";
import "./App.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
web3: null,
accounts: null,
contract: null,
name: null,
age: null,
hobby: null,
address: "",
outputName: null,
outputAge: null,
outputHobby: null,
};
}
componentDidMount = async () => {
try {
const web3 = await getWeb3();
const accounts = await web3.eth.getAccounts();
const networkId = await web3.eth.net.getId();
const deployedNetwork = Resister.networks[networkId];
const instance = new web3.eth.Contract(
Resister.abi,
deployedNetwork && deployedNetwork.address
);
this.setState({ web3, accounts, contract: instance });
} catch (error) {
alert(
`Failed to load web3, accounts, or contract. Check console for details.`
);
console.error(error);
}
const { accounts } = this.state;
console.log(accounts);
};
// アカウント情報の登録
writeRecord = async () => {
const { accounts, contract, name, age, hobby } = this.state;
const result = await contract.methods.registerAccount(name, age, hobby).send({
from: accounts[0],
});
console.log(result);
if (result.status === true) {
alert('会員登録が完了しました。');
}
};
// アカウント情報の読み込み
viewRecord = async () => {
const { contract, accounts } = this.state;
console.log(contract);
const result = await contract.methods.viewAccount(accounts[0]).call();
console.log(result);
const outputName = result[0];
const outputAge = result[1];
const outputHobby = result[2];
this.setState({ outputName, outputAge, outputHobby });
};
handleChange = (name) => (event) => {
this.setState({ [name]: event.target.value });
};
render() {
return (
<div className="App">
<br />
<form>
<div>
<label>氏名:</label>
<input
onChange={this.handleChange("name")} />
</div>
<div>
<label>年齢:</label>
<input
onChange={this.handleChange("age")} />
</div>
<div>
<label>趣味:</label>
<input
onChange={this.handleChange("hobby")} />
</div>
<button type='button' onClick={this.writeRecord}>
会員登録
</button>
</form>
<br />
<br />
<form>
<label>検索したいアドレスを入力してください。</label>
<input onChange={this.handleChange("address")} />
<button type='button' onClick={this.viewRecord}>
検索
</button>
</form>
<br />
<br />
{this.state.outputName ? <p>氏名: {this.state.outputName}</p> : <p></p>}
{this.state.outputAge ? <p>年齢: {this.state.outputAge}</p> : <p></p>}
{this.state.outputHobby ? <p>趣味: {this.state.outputHobby}</p> : <p></p>}
</div>
);
}
}
export default App;
ここで確認のために起動を行います。npm start
を打ち込みます。
すると以下のように表示されるはずです。
シンプルなUIが完成しました。👏
ここまでのreact-box/client/src内のファイル構成はこのようになっています。
#テスト
では、実際に動かしてみましょう。
その前に!MetamaskとReact Appとの接続を許可する必要があります。
-
Metamaskを起動。
-
画像上部のネットワーク名を
Rinkebyテストネットワーク
に設定。 -
水色で囲まれた右上のボタンをクリック→、
Connected sites
をクリック→、Manually connect to current site
→、次へ→、Connect
。
では、いよいよテストしていきます。
適当に氏名、年齢、趣味を入力して会員登録ボタンを押してみてください。
自動的にmetamaskが起動するので確認ボタンを押してください。
すると会員情報がブロックチェーン上に記録されます。ディベロッパーツールでは、ブロックに関する情報を見ることができます。
次に登録した情報を閲覧をしてみましょう。metamaskを開いてアドレスをコピーします。
コピーしたアドレスを入力して検索ボタンを押してみてください。
会員情報の閲覧ができました!!!👏👏
次は、このアプリケーションのデザインをカッコよくしていきたいと思います。
#React-Bootstrapを用いてアプリケーションの見た目をカッコよくしていく
ここから先は、冒頭で紹介したようなアプリケーションの見た目にしていく作業になります。
(※アプリケーションの見た目にこだわらない方であればこのセクションはとばしても全然問題ないです)
では、React-Bootstrapを用いてApp.jsを編集していきます。
react-bootstrapのインストール方法と使い方は以下の記事を参考にして下さい。
React Bootstrap 公式ドキュメント
編集後のApp.jsがこちらになります。
import React from "react";
import Resister from "./Resister.json";
import getWeb3 from "./getWeb3";
import { Row, Col, Button, Form, Modal } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
web3: null,
accounts: null,
contract: null,
name: null,
age: null,
hobby: null,
address: "",
outputName: null,
outputAge: null,
outputHobby: null,
////
lines: [],
// モーダル
show: false,
// フォームチェック
validated: false,
};
}
// モーダル設定
handleClose = async () => {
await this.setState({ show: false });
// ページリロード
document.location.reload();
}
handleShow = async () => this.setState({ show: true });
componentDidMount = async () => {
try {
const web3 = await getWeb3();
const accounts = await web3.eth.getAccounts();
const networkId = await web3.eth.net.getId();
const deployedNetwork = Resister.networks[networkId];
const instance = new web3.eth.Contract(
Resister.abi,
deployedNetwork && deployedNetwork.address
);
this.setState({ web3, accounts, contract: instance });
} catch (error) {
alert(
`Failed to load web3, accounts, or contract. Check console for details.`
);
console.error(error);
}
const { accounts, contract } = this.state;
console.log(accounts);
const item = await contract.methods.accounts(accounts[0]).call();
this.state.lines.push({
item
});
console.log(this.state.lines);
};
// アカウント情報の登録
writeRecord = async () => {
const { accounts, contract, name, age, hobby } = this.state;
const result = await contract.methods.registerAccount(name, age, hobby).send({
from: accounts[0],
});
console.log(result);
if (result.status === true) {
this.handleShow();
}
};
// アカウント情報の読み込み
viewRecord = async () => {
const { contract, accounts } = this.state;
console.log(contract);
const result = await contract.methods.viewAccount(accounts[0]).call();
console.log(result);
const outputName = result[0];
const outputAge = result[1];
const outputHobby = result[2];
this.setState({ outputName, outputAge, outputHobby });
};
handleChange = (name) => (event) => {
this.setState({ [name]: event.target.value });
};
// フォーム最終確認
handleSubmit = (event) => {
const form = event.currentTarget;
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
this.setState({ validated: true });
};
render() {
return (
<div className="App">
<Row className="text-left m-5">
<Col md={{ span: 4, offset: 2 }}>
<Form className="justify-content-center"
noValidate validated={this.state.validated} >
<Form.Group controlId="validationCustom03">
<Form.Label>Name</Form.Label>
<Form.Control
type="name"
onChange={this.handleChange("name")}
placeholder="Enter Name"
required />
<Form.Control.Feedback type="invalid">
Please enter name.
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="validationCustom03">
<Form.Label>Age</Form.Label>
<Form.Control
type="text"
onChange={this.handleChange("age")}
placeholder="Enter Age"
required />
<Form.Text className="text-muted">
We'll never share your age with anyone else.
</Form.Text>
<Form.Control.Feedback type="invalid">
Please enter age.
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="validationCustom03">
<Form.Label>Hobby</Form.Label>
<Form.Control
type="text"
onChange={this.handleChange("hobby")}
placeholder="Enter Hobby"
required />
<Form.Control.Feedback type="invalid">
Please enter hobby.
</Form.Control.Feedback>
</Form.Group>
{/* フォームチェック */}
<Form.Group>
<Form.Check
required
label="Agree to terms and conditions"
feedback="You must agree before submitting."
onChange={this.handleSubmit}
/>
</Form.Group>
<Button variant="primary" type="button" onClick={this.writeRecord}>
会員登録
</Button>
{/* モーダル */}
<Modal show={this.state.show} onHide={this.handleClose}>
<Modal.Header closeButton>
<Modal.Title>Wellcome to BcDiary!!</Modal.Title>
</Modal.Header>
<Modal.Body>会員登録が完了しました。</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={this.handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</Form>
</Col>
<Col md={{ span: 4, offset: 0 }} className='ml-5'>
<Form className="justify-content-center">
<Form.Group controlId="formBasicEmail">
<Form.Label>検索したいアドレスを入力してください。</Form.Label>
<Form.Control onChange={this.handleChange("address")}
placeholder="Search" />
</Form.Group>
<Button variant="primary" type="button" onClick={this.viewRecord}>
閲覧
</Button>
</Form>
<br />
<br />
{this.state.outputName ? <p>Name: {this.state.outputName}</p> : <p></p>}
{this.state.outputAge ? <p>Age: {this.state.outputAge}</p> : <p></p>}
{this.state.outputHobby ? <p>Hobby: {this.state.outputHobby}</p> : <p></p>}
</Col>
</Row>
</div >
);
}
}
export default App;
#DApp完成!
上記のプログラムの元、完成したアプリケーションはこのようになります。
Githubにソースコードをあげているのでそちらも良ければ参考にして下さい。
#最後に
ここまで記事を最後まで読んで頂いて本当にありがとうございます!そしてお疲れ様です!
初めてDApps開発をする方にとって少しでも力になっていれば幸いです。
是非LGTMボタンをポチッとお願いします!!