3
@sho11hei12-1998

【初めてのDApps開発②】ReactでシンプルなDAppsを作ってみよう!~フロントエンド編~

はじめに

[※注意!!]この記事は【初めてのDApps開発①】の続編です。まだ読んでいない人はそちらを読んでからこの記事を読んで下さい。

今回は最後の項目のReactを用いたフロントエンド開発を行います。

  1. Remixを用いたスマートコントラクト開発
  2. Truffleを用いたスマートコントラクトのデプロイ
  3. Reactを用いたフロントエンド開発

「Reactというフロントエンドフレームワークを使ってweb3.jsを経由してmetamaskを動かし, スマートコントラクトを動作させる」ということをやっていきます。
つまりこのようなイメージになります。

作っていくアプリケーション

本記事では、【初めてのDApps開発①】でデプロイしたスマートコントラクトを使いながらアプリケーションの見た目の部分を作っていきます。

完成するアプリケーションは、以下のようになります。
URL: https://diarydapp-2a69c.web.app

スクリーンショット 2020-11-29 11.29.27.png

では、一緒に作っていきましょう!!!

セットアップ

まずディレクトリを作ります。

そこに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ファイルと一まとめにして管理することにしました!
スクリーンショット 2020-12-02 19.49.06.png

さて、【初めてのDApps開発①】で作成したdapps-deploy ディレクトリ下の build/contractResister.json ファイルが生成されています。これはコントラクトのコンパイル時に生成されたものです。中身を丸ごとコピーしましょう。

そして react-boxディレクトリのclient/src 下に Resister.json というファイルを作ってそこに貼ります。

Reactを記述していく!

まず、client/src 下の App.js をこのように書き換えましょう。

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がこちらです。↓

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を打ち込みます。
すると以下のように表示されるはずです。

スクリーンショット 2020-11-29 9.59.48.png

シンプルなUIが完成しました。👏

ここまでのreact-box/client/src内のファイル構成はこのようになっています。
スクリーンショット 2020-12-02 19.43.50.png

テスト

では、実際に動かしてみましょう。
その前に!MetamaskとReact Appとの接続を許可する必要があります。

  1. Metamaskを起動。
  2. 画像上部のネットワーク名をRinkebyテストネットワークに設定。

  3. 水色で囲まれた右上のボタンをクリック→、Connected sitesをクリック→、Manually connect to current site→、次へ→、Connect

  4. 赤枠で囲った箇所がConnectedに変わっていれば、React Appとの接続が完了しています。
    スクリーンショット 2020-11-29 10.21.44.png

では、いよいよテストしていきます。
適当に氏名、年齢、趣味を入力して会員登録ボタンを押してみてください。

スクリーンショット 2020-11-29 10.39.46.png

自動的にmetamaskが起動するので確認ボタンを押してください。
すると会員情報がブロックチェーン上に記録されます。ディベロッパーツールでは、ブロックに関する情報を見ることができます。

スクリーンショット 2020-11-29 10.06.45.png

スクリーンショット 2020-11-29 10.14.39.png

次に登録した情報を閲覧をしてみましょう。metamaskを開いてアドレスをコピーします。

スクリーンショット 2020-11-29 10.41.23.png

コピーしたアドレスを入力して検索ボタンを押してみてください。

スクリーンショット 2020-11-28 15.33.19.png

会員情報の閲覧ができました!!!👏👏

次は、このアプリケーションのデザインをカッコよくしていきたいと思います。

React-Bootstrapを用いてアプリケーションの見た目をカッコよくしていく

ここから先は、冒頭で紹介したようなアプリケーションの見た目にしていく作業になります。
(※アプリケーションの見た目にこだわらない方であればこのセクションはとばしても全然問題ないです)

では、React-Bootstrapを用いてApp.jsを編集していきます。
react-bootstrapのインストール方法と使い方は以下の記事を参考にして下さい。
React Bootstrap 公式ドキュメント

編集後のApp.jsがこちらになります。

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にソースコードをあげているのでそちらも良ければ参考にして下さい。

スクリーンショット 2020-11-29 11.29.27.png

最後に

ここまで記事を最後まで読んで頂いて本当にありがとうございます!そしてお疲れ様です!
初めてDApps開発をする方にとって少しでも力になっていれば幸いです。
是非LGTMボタンをポチッとお願いします!!

3
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
sho11hei12-1998
春からエンジニアとして働く大学生です。 C, Java, Python, Solidity, HTML, CSS, JavaScript, React, React-native 少しづつかじりました。 Twitter ▶︎ @shohei_saginao
この記事は以下の記事からリンクされています

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
エンジニアによるマネジメント - エンジニアだからこそ発信できるマネジメントの知識を発信しよう
~
3000文字Tips - 知ると便利なTipsをみんなへ届けよう
~