20201207のReactに関する記事は14件です。

Reactで画像を表示させる方法

近頃Reactでいいサイトを作ろうとしているのですが、画像を表示させるのに3日かかってしまったのでたどり着いたコードを書いていきます。

表示のさせ方

test.react
import ReactDOM from 'react-dom';
import Icon from './logo.png';
const IconComponent = () =>{
  return <img src={Icon}  alt="アイコン" />
}
const rootElement = document.getElementById("root");
ReactDOM.render(
  <IconComponent />,rootElement
);

こんな感じになりました。
import Icon from ''でパスを指定するとそのものがIconに
入るみたいです。
const rootElement = document.getElementById("root");
をしてからReactDOM.renderで、
,rootElement
としないとTarget container is not a DOM element
と出ます。ここら辺の理解はまだまだなのでこれから調べます。

終わり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

はじめに

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

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

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

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

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

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

完成するアプリケーションは、以下のようになります。

スクリーンショット 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ボタンをポチッとお願いします!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WEB開発未経験から一人でアプリを作りました

この記事は「NEXTSCAPE Advent Calendar 2020」の11日目の記事です

WEB開発未経験の私が、基礎的なWEB開発の知識を身に着けるために書評WEBアプリを題材に作成しました。

この記事では最初にアプリの紹介をしたのちに

  • インフラ構成について
  • 書籍照会のためにGoogle Books APIsを採用
  • React Hooksで自分がハマってしまったポイントについて

といったところについて、お話ししようと思います。

どんなアプリを作ったのか

すでにお話に出ていますが、書評WEBアプリです。
ユーザーが本についてのレビューを書き込み、他のユーザーと共有できるといったものです。

なぜこのアプリを作ったのか

ネクストスケープでは本を読む方が多く、お互いに本を薦めることが頻繁にあると聞いたからです。せっかく作るなら、使っていただける可能性のあるテーマで作成するほうがより、モチベーション持って続けられると感じたからです。

アプリの写真と紹介

トップページ

トップページでは新着レビューを閲覧したり、人気のレビューをランキング形式で見れるようにしてみました。
image.png

レビューページ

レビューを選択することで、詳細の閲覧やレビューに対して「いいね」することもできます。
image.png

マイページ

マイページでは投稿したレビューを管理できます。また、自分が過去にいいねしたレビューも閲覧できます。
image.png

使用技術

フロントエンド

React, Material-UI

サーバーサイド

.NET Core3.1

インフラ

Microsoft Azure, AzureDevOps

サーバーサイドは、.NET Core で作成しました。
オブジェクト指向設計を意識して、業務システムを作成する際の練習になるようにしました。

また、AzureDevOpsを用いてパイプラインを作成し、masterブランチにpushされたら、ビルド、単体テスト、デプロイが自動で実行されるようにしました。

インフラ構成について

image.png

書籍照会のためにGoogle Books APIsを採用

アプリで書籍の情報を取得する必要がありました。
当初は以下の三つで比較・検討

それぞれ実装の容易さ、書籍情報量、書籍情報の質を中心に紹介できればと思います。

まず、紹介に入る前にざっと内容をまとめてみました。

実装の容易さ 書籍情報量 書籍情報の質
Google Books APIs 認証必要なし ページ数やカテゴリもとれる ISBNがなかったり、フォーマットがまばら。JSONで取れる
楽天ブックス書籍検索API 認証あり ページ数が取れない レスポンスが綺麗(ただしAPIによって形式が異なる)JSONで取れる
Amazon Product Advertising API APIの内容みるために登録が必要 未検証 未検証(レスポンスはxml形式のみ)

Google Books APIs

単純な書籍情報が欲しい時に一番おすすめです。APIを利用する際に登録は不要で
https://www.googleapis.com/books/v1/volumes?q=【ここに書籍名または著者など】
だけで、すぐに使えます。
ここを押してみてください⇒https://www.googleapis.com/books/v1/volumes?q=相沢沙呼

また、書籍情報に関しても書籍のカテゴリや、ページ数も取得できるので結構充実してると思います。
しかし、ISBNがなかったり、フォーマットが決まっていないこと(123-xxxx or 123xxxxのようにハイフンがない場合)があるので注意は必要になると思います。
ちなみに、レスポンスはJSON形式で取れます。

今回は実装の容易さと情報量がある程度欲しいことから、こちらを採用しました。

楽天ブックス系API

楽天ブックスAPIを利用するためには、事前にアプリ情報の登録をしてアプリIDの発行が必要になります。
用途に合わせてAPIが複数あるので、ややこしいと感じました。
(それぞれレスポンス構成が異なる)

少し触ってみたところ、書籍のページ数が取れないという点もあり、もし必要な場合はほかのAPIと組み合わせる必要があります。

良い点としてはISBNはきちんと取得できて、また、書籍以外の検索にも利用できそうでありました。
GoogleBooksAPIsとの比較はISBNをきちんと管理したいかで判断するとよいと思います。
ちなみに、レスポンスはJSON形式で取れます。

Amazon Product Advertising API

まず、APIの内容を確認しようとアクセスすると登録からするように案内されました。
また、レスポンスもxmlのみしか対応していないみたいです。

今回は内容を確認する前に登録が必要なこともあり、上記二つと比較して容易に扱えないと判断しました。

React Hooksで自分がハマってしまったポイントについて

書評WEBアプリではフロントエンドの開発にReactを採用しております。
参考にさせていただいたサイト

useEffect内での非同期関数の書き方

非同期関数をuseEffect内で直接呼ぶことはできない!!!
つまり、下記のような書き方はできないということです。

sampleNgUseEffect.js
React.useEffect(async () => {
  await func1();
}, [fun1]);

なぜ、できないかといいますと、async で定義された非同期関数は、返り値に Promise オブジェクトを返す通常の関数のシンタックスシュガーだから、という点です。
useEffectに渡す関数の戻り値はcleanup関数だからです。

useEffect内で非同期関数を使う場合は、下記のように非同期関数を定義してから実行しましょう。

sampleUseEffect.js
React.useEffect(() => {
  // 1)定義
  const func1 = aysnc () => {
    // ...
    await ...
    // ...
  }

  // 2)実行
  func1()

}, [...])

useStateのsethogehoge()は即座に反映してくれない

useEffectと合わせて、内部でsethogehoge()を使うことが多々あると思いますが、今回のNGな書き方を下記に載せます。

useStateNg.js
const [userId, setUserId] = React.useState('default');
React.useEffect(() => {
  setUserId('newValue');
  console.log(`userId is ${userId}`); 
}, [userId]);

この場合logとして出力されるのは、「userId is default」となります。
なぜかといいますと、その関数が実行された時点のuserIdを使うからです。

setした値を利用したい場合は、変数に置き換えて利用するなどの工夫が必要です。

useState.js
const [userId, setUserId] = React.useState('default');
React.useEffect(() => {
  const newValue = 'newValue'; // << ここで変数に入れてあげる

  setUserId(newValue);
  console.log(`userId is ${userId}`); 
}, [userId]);

まとめ

今後、増やしたい機能

  • 別のユーザーにこの本おすすめだよといったレコメンド機能
  • レビューに対してコメント機能
  • 他ユーザーのページ(その人のレビューとかいいねしているのが見れる)
  • カテゴリやタグなんかも入れたい

ここまでお読みいただきありがとうございます。今回は局所的であまり参考にはならない内容になってしまいましたが、書籍管理をどうしようと考えている方、React Hooksで苦戦中の方に少しでも参考になればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Fast Refresh を有効にする

以前から react-hot-loader を使って、 HMR (Hot Module Reloading) を行ってきましたが、
ReactFast Refreshというネイティブ機能が提供されましたので、こちに移行します。
今回は webpack を使った場合、具体的にな設定を説明します。

ライブラリインストール

Fast Refresh 機能は、react-refresh を追加でインストール必要です。
webpack使う場合、babel とプラグインもインストール必要がある

Install
yarn add react-refresh
yarn add @babel/core babel-loader @pmmmwh/react-refresh-webpack-plugin -D

設定

意外と設定する場所が少ないが、プラグインなしの場合死ぬほど難しいそうです。

webpack.ts
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';

const configs: Configuration = {
  ...
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            // *** ここから ***
            options: { plugins: ['react-refresh/babel'] },
            // *** ここまで ***
          },
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    ...
    // *** ここから ***
    new ReactRefreshWebpackPlugin(),
    // *** ここまで ***
  ],
  ...
};

試して見る

デモ用ソースは GitHub で公開中です。
https://github.com/wwalpha/react-refresh-typescript-example

DEMO
git clone https://github.com/wwalpha/react-refresh-typescript-example.git
cd react-refresh-typescript-example
yarn install
yarn start

全部5種類コンポーネントがテストできます。

  • Class Default
  • Class Named
  • Function Default
  • Function Named
  • Lazy Component
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebaseでfunctionsの関数をdeploy時にasyncのエラー

firebaseでデプロイ時にfuncionsへ関数のデプロイに失敗

エラー文

Function failed on loading user code. Error message: Code in file lib/index.js can't be loaded.
Is there a syntax error in your code?
Detailed stack trace: /srv/node_modules/@google-cloud/firestore/build/src/collection-group.js:54
    async *getPartitions(desiredPartitionCount) {
          ^

SyntaxError: Unexpected token *
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:617:28)
    at Object.Module._extensions..js (module.js:664:10)
    at Module.load (module.js:566:32)
    at tryModuleLoad (module.js:506:12)
    at Function.Module._load (module.js:498:3)
    at Module.require (module.js:597:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/srv/node_modules/@google-cloud/firestore/build/src/index.js:39:28)

⚠  functions: Warning: Node.js 8 functions are deprecated and will stop running on 2021-03-15. Please upgrade to Node.js 10 or greater by adding an entry like this to your package.json:

    {
      "engines": {
        "node": "12"
      }
    }

The Firebase CLI will stop deploying Node.js 8 functions in new versions beginning 2020-12-15, and deploys from all CLI versions will halt on 2021-02-15. For additional information, see: https://firebase.google.com/support/faq#functions-runtime



Functions deploy had errors with the following functions:
    addDataset


To try redeploying those functions, run:
    firebase deploy --only "functions:addDataset"


To continue deploying other features (such as database), run:
    firebase deploy --except functions

Error: Functions did not deploy properly.

async *getPartitions(desiredPartitionCount)
非同期通信の関数データ付近でエラーが起きているとわかった。

エラー対処方法

functionsのpackage.jsonの中身を

"dependencies": {
    "firebase-admin": "^9.2.0",
    "firebase-functions": "^3.11.0"
  },
  "devDependencies": {
     "@typescript-eslint/eslint-plugin": "^3.9.1",
    "@typescript-eslint/parser": "^3.8.0",
    "eslint": "^7.6.0",
    "eslint-plugin-import": "^2.22.0",
    "typescript": "^3.8.0",
    "firebase-functions-test": "^0.2.0"
  },

から

"dependencies": {
    "firebase-admin": "^8.6.0",
    "firebase-functions": "^3.3.0"
  },
  "devDependencies": {
    "tslint": "^5.12.0",
    "typescript": "^3.2.2",
    "firebase-functions-test": "^0.1.6"


削除及び修正し変更。

eslintと相性があわなかったのかもっと予想・・・。
Typescriptに詳しくないので、詳しいことがわかりません。

ヒントになった参考記事

https://stackoverflow.com/questions/64575650/firebase-functions-error-after-update-what-can-i-do

以上となります。

ここは違う、ここはこうしたほうがいいかも?
等々ございましたら、ご指摘いただけますと幸いです。

最後までみていただきありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでファイル拡張子を".js"から".jsx"にするとエラーが起きる(Error: ENOENT: no such file or directory, open '実行パス')

はじめに

こんにちは。Qiita3記事目です。
最近はよく起きるエラーの解決法備忘録集みたいになってきています…笑
今回もまたよく起きそうなエラーが発生したのでその解決策をまとめておきます。

Markdown記法にも少しずつ慣れてきました。

開発環境

  • エディタ:VSCode
  • OS:Mac OS X
  • node:v15.2.1
  • yarn:1.22.10
  • React:17.0.1
  • React-dom:17.0.1
  • React-script:4.0.1

結論(解決策)

そのプロジェクトフォルダ内のnode_modulesを削除し、yarn installしてパッケージをインストールし直す。
npmを使っている方はyarn installの部分をnpm installに。

$ rm -rf node_modules && yarn install

原因:拡張子を".js"から".jsx"にすると発生する

VSCodeでは拡張子に応じてファイルのアイコンを変更してくれる機能があります。
コンポーネントを記述したファイルは拡張子を.jsxにすることで、ファイルアイコンがReactのアイコンになり判別がしやすくなるということで早速変えてみたのですが、以下のようなエラーが出ました。

App.jsをApp.jsxに変更したあとのターミナル
./src/App.js
Error: ENOENT: no such file or directory, open '/Users/creditbba/Desktop/programming/practice/js-react/jake-react/react-practice/src/App.js'

解決策

解決策としては、一度プロジェクトフォルダ内のnode_modulesを削除し、再度yarn installでパッケージをインストールし直すことによって解決しました。

$ rm -rf node_modules && yarn install

-rオプションは「ディレクトリを削除する」オプションで、-fオプションは「メッセージを表示しない(「本当に削除していいですか?」みたいなもの)」オプションになります。

懸念点

そんなに頻繁にやることではないと思うのでよいのですが、この拡張子の変更によるエラーは原因となる拡張子を変更する度に毎回起こるということを何回か実験して確認しました。

例としては、今回のような.jsから.jsxへの変更、逆に.jsxから.jsへの変更、また.jsxから.tsxへの変更など、これらの変更を行った段階で今回のエラーが発生します。

その度に今回の解決策のコマンドを実行すれば解決はするのですが、如何せん面倒臭いなあという気もします。

何かもっと良い解決策はないのでしょうかね。

今回参考にしたもの

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのカラーテーマはこうしてこう[Material-UI, styled-componentsの例]

TL;DR

この記事は、メタップス Advent Calendar 20207日目の記事です。
本当にやりたかったテーマ(Reactで新規プロジェクト立ち上げるときはこうしてこう)に間に合わなかったため、やや細かい内容になってしまいました。お許しください。

ここでは、ReactでThemingするとき、カラーテーマをどんな感じでやると良いか、Material-UIやstyled-componentsのThemeProviderを例にとって、In My Opinionで解説します!

方針

原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。

  • primaryyssecondaryは、プロジェクトで決めているテーマカラーを素直に入れていく
  • それで足りない色は型を注入してユーザー定義していく
  • Material-UIとstyled-componentsのThemePrivider両方で共通のthemeオブジェクトを利用する
  • 当然typescriptでコードヒントを効かす

解説と実装例

Material-UIのThemeProvider

Material-UIのColor paletteは、primary, secondary, error, warning, ...と存在し、以下のような構造のインタフェースになっています。

export interface SimplePaletteColorOptions {
  light?: string;
  main: string;
  dark?: string;
  contrastText?: string;
}

...

export interface Color {
  50: string;
  100: string;
  200: string;
  300: string;
  400: string;
  500: string;
  600: string;
  700: string;
  800: string;
  900: string;
  A100: string;
  A200: string;
  A400: string;
  A700: string;
}

...

export type ColorPartial = Partial<Color>;

...

export type PaletteColorOptions = SimplePaletteColorOptions | ColorPartial;

...

export interface PaletteOptions {
  primary?: PaletteColorOptions;
  secondary?: PaletteColorOptions;
  ...
}

かなり細かく設定できますが、primary、secondaryを除けば、infoやsuccessなどの汎用的なカラーリングであり、プロジェクト規模がある程度まで行くと、これでは足りなくなってしまいます。

例えば、Material-UIで定義されているパレットに加えて、red, pink, greenを、定義済みのパレットと同じようなインタフェースで新設したいときは、どうすればよいのでしょうか。

たまに見かけるやり方で、、theme.tsxなどに、Material-UIのcreateMuiThemeとは別にカラーのオブジェクトを定義してエクスポートし、利用する場所でimportするというのがあります。

theme.tsx
export const theme = createMuiTheme({
...
});

export const userPalette = {
    red: {
        ...
    }
}

しかし、毎回importするのは面倒ですし、これではコードヒントも効きません。
themeはネストが深くなるので、きちんと型をつけてコードヒントの恩恵を受けたいです。

なのでここは型を注入します。

types/createPalette.d.ts
import * as createPalette from "@material-ui/core/styles/createPalette";
import { PaletteColorOptions } from "@material-ui/core/styles/createPalette";

interface CustomPalette {
  red: PaletteColorOptions; // light, main, dark, 50, 100, ..., 900, A100, ..., A700
  pink: PaletteColorOptions;
  green: PaletteColorOptions;
}

declare module "@material-ui/core/styles/createPalette" {
  // eslint-disable-next-line
  interface PaletteOptions extends CustomPalette {}
  // eslint-disable-next-line
  interface Palette extends CustomPalette {}
}

これで型が拡張されました。
ThemePrivider.tsx で、拡張した型を追加でthemingしてみます。

またここでも、Material-UIのカラーパレットシステムを利用します。(自分で一つずつ設定しても良いですが)
Material-UIでは、ある程度一般的な色のパレットをimportできるように準備されています。
https://material-ui.com/customization/color/#color-palette

これを利用すると、以下のように非常にシンプルに記述することができます。

ThemePrivider.tsx
import { red, pink, green } from "@material-ui/core/colors";

export const theme = createMuiTheme({
  palette: {
    red: {
      ...red,
      light: "#e57373",
      main: "#f44336",
      dark: "#d32f2f",
    },
    pink: {
      // redと同様
    },
    green: {
      // redと同様
    },
  },
});

これで新たに、17色 * 3の色がテーマから使えるようになっています。

Screen Shot 2020-12-07 at 6.30.52.png

あとは、受け取ったchildrenをラップするようにコンポーネントを定義し、ルートコンポーネントでラップするだけです。

ThemeProvider.tsx
import { ThemeProvider as MuiThemeProvider } from "@material-ui/styles";

export const ThemeProvider: React.FC<Props> = ({ children }: Props) => {
  return (
    <MuiThemeProvider theme={createMuiTheme(theme)}>
      {children}
    </MuiThemeProvider>
  );
};

styled-componentsのThemeProvider

以上でMaterial-UIでのThemingはできました。
次はstyled-componentsのThemeProviderにも、同じthemeオブジェクトを渡して、styled内でpropsとして利用できるようにしたいです。

しかし、そのままThemeProviderにthemeを渡すと、tscでエラーが出るので、ここでも型を注入します。

Material-UIのThemeインタフェースを利用してシンプルに書くことができます。

types/styled-components.d.ts
import "styled-components";
import { Theme } from "@material-ui/core";

declare module "styled-components" {
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
  export interface DefaultTheme extends Theme {}
}

こうしておいて、ThemeProviderでさらにラップすれば良いです。

ThemeProvider.tsx
import { ThemeProvider as MuiThemeProvider } from "@material-ui/styles";
import { ThemeProvider as StyledThemeProvider } from "styled-components";

export const ThemeProvider: React.FC<Props> = ({ children }: Props) => {
  return (
    <MuiThemeProvider theme={createMuiTheme(theme)}>
      <StyledThemeProvider theme={theme}>{children}</StyledThemeProvider>
    </MuiThemeProvider>
  );
};

あとは普通に使うだけ。
styled-componentsのThemeProviderは、下位にある全てのコンポーネントツリーに対してこのthemeを送るため、全てのstyledでthemeにアクセスできるようになり、importする必要はなくなります。
コードヒントも有効になります。

const StyledRedWrapper = styled.div`
  background-color: ${(props) => props.theme.palette.red[50]};
`;

おわり

これで、Material-UIのテーマを拡張して適用し、また、styled-compoenentsでも同じものを利用できるようになりました。
今回はMaterial-UI & styled-componentsでしたが、Material-UIがAntDesignでも、styled-componentsがemotionでも、あるいはどちらか片方しか使わなくても、やることは基本的に同じだと思います。

方針に書きましたが、

原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。

で良いのかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(React)npm start起動時のCORS回避

前提

フロントエンド - React.js
バックエンド - Go

問題

npm startでサーバを起動した際、React App内で外部APIをfetchすると、CORSエラーが発生する。

すでに行った対処(参考にした記事)

  • fetchの第二変数に{mode: 'cors'}を追加する。
  • GoのResponse Headerにw.Header().Set("Access-Control-Allow-Origin", "*")を追加。

この二つの対処では、CORSエラーは解消されなかった。

解決法

(ここに書いてあった。プロキシを挟むと良い。)

ブラウザからのAPIの送り先がlocalhost:4000の場合は、React Appのpackage.jsonに、
"proxy": "http://localhost:4000",
と追加で記述すると良い。

fetchでlocalhost:4000/apiにリクエストを投げたい時は、
fetch("/api", {mode: 'cors'}).then....
と書く。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け]React Redux Basic Tutorial�にあるTodoAppを今書き直したら

はじめに

枠が空いていましたので、Advent Calendarに参加させていただきました。
10月からReactの学習を始めた新参者です。
間違っているところなどあればご指摘頂けると幸いです。

React Reduxを学ぼうと思った時に Basic Tutorial | React Redux にあるTodoAppをみる方も多いと思います。
私はReactの学習を くるみ割り書房 ft. React - BOOTH さんから出ているりあクト!シリーズで勉強しているのですが、改めてBasic TutorialのTodoAppを見るとこれはこのまま実務のコードとして参考に出来ないなと思いました。
例えば、function TodoApp()など関数でなく、アロー関数で書くなど。
これからReactでプロジェクトを進めていく時はおそらくどこもアロー関数になると思います。

これからReactを学ぶ上で最小限の小さいアプリを作っておいて見返すのは大事だと思いますので、せっかくならReact Redux Basic TutorialのTodoAppを書き直そうと思いました。

記事では、下記リポジトリの補足コメントを書いてるところが大きいです。
https://github.com/nori1994/todo-app-react

TodoApp

Basic Tutorial | React Redux の全ての機能が揃っているわけでなく、最低限React Reduxとは?が分かる範囲で書いています。
そして、りあクト!の11-redux/04-toolkit/src/features/todo.tsで紹介されているSliceを実際に使うとどうなるのかを検証したかったためコードの参考引用しているところがあります。

Slice

Sliceは、Action と Reducerをまとめて定義できるためすっきり記述できます。

Action と Reducerとは?という方は下記にざっくりと書いたので参考になれば嬉しいです。
Action
Actionは、イベント、例えばStateをどう変化させるかを定義します。
ActionType(文字列)からActionCreatorと呼ばれる Actionを作る関数を生成します。

Reducer
Reducerは、以下のように古いStateとActionから新しいStateを取得するようになっています。

type Reducer<S, A> = (state: S, action: A) => S

Three Principles | Redux
Actionは、Dispatcherを介してReducterに渡されます。

createSliceを使うとActionTypeの定義とActionCreator、Reducerをまとめて定義できます。
今回のTodoAppにはTodoを管理する、表示内容をフィルターするの大きく2つの機能があると考えたので、それぞれSliceを生成して、combineReducersで1つのReducerにしました。
createSlice | Redux Toolkit
combineReducers | Redux

例えば表示内容をフィルターする場合は、以下のようにAction と Reducerを定義しました。

visibilityFilterSlice.ts
export const visibilityFilterSlice = createSlice({
  name: 'visibilityFilter',
  initialState: {
    visibilityFilter: VISIBILITY_FILTER.INCOMPLETE,
  } as VisibilityFilterState,
  reducers: {
    changed: (state, action: PayloadAction<VISIBILTY_FILTER_TYPE>) => {
      const changedValue = action.payload;
      state.visibilityFilter = changedValue;
    },
  },
});

Store

Storeは、アプリでの状態Stateを保管するためのものです。
configureStoreでStoreを生成しています。
Stateの変化を履歴で表示してくれるデバックツールRedux DevTools extension がデフォルトでONになっているなど設定が優秀なので、configureStoreでStoreを作っています。

configureStoreなどに含まれている内容は以下を参照。
Redux Toolkit: Overview | Redux

index.tsx
const rootReducer = combineReducers({
  todo: todoSlice.reducer,
  visibilityFilter: visibilityFilterSlice.reducer,
});

const store = configureStore({ reducer: rootReducer });

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);

Dispath

実際にStateを変更するにはDispatcherを介してReducterにActionが渡されることで変更されます。

VisibilityFilters.tsx
const VisibilityFilters: FC = () => {
  const dispatch = useDispatch();
  const { changed } = visibilityFilterSlice.actions;
  const visibilityFilterState = useSelector(
    (state: RootState) => state.visibilityFilter,
  );

  return (
    <div>
      {Object.entries(VISIBILITY_FILTER).map(([_, currentFilter]) => {
        return (
          <text
            key={`visibility-filter-${currentFilter}`}
            className={`filter ' + ${
              currentFilter === visibilityFilterState.visibilityFilter
                ? 'filter--active'
                : ''
            }`}
            onClick={() => {
              dispatch(changed(currentFilter as VISIBILTY_FILTER_TYPE));
            }}
          >
            {currentFilter}
          </text>
        );
      })}
    </div>
  );
};

まとめ

この小規模単位のアプリを手元に持っておけばこれどうなっているんだっけとなった時に振り返りやすくなったと感じています。
あと、くるみ割り書房 ft. React - BOOTHさんで出ている本は旧版を除いて全て購入しました。TodoAppも多大に影響を受けています。とてもおすすめです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【脱create-react-app】React開発環境を1から作ってみる

はじめに

この記事は限界開発鯖 Advent Calendar 2020の7日目です。
何気にAdvent Calendar初挑戦です。

もともと「高速化のためのWebpack」というタイトルでWebpackのチューニングについて書こうと思っていたのですが、あまりにもニッチ過ぎたのでやめました。

1コマンドで環境構築ができるcreate-react-app

皆さんReactの開発はやったことありますか?Reactのチュートリアルでは環境構築としてcreate-react-appが紹介されています。このcreate-react-appは以下のコマンドを叩くだけですぐにReactの開発環境を作れる夢のようなアイテムです。

npx create-react-app my-app

このコマンドはたしかにReact初心者の方にはおすすめのコマンドですが、React(Webフロント)中級者になってくると、ビルド時のコンフィグをいじったり新しいツールチェインを導入したくなると思います。
create-react-appはコンフィグをほとんど隠してしまうため、いじることのできる箇所が非常に少ないです。

一応npm run ejectcreate-react-appが隠しているコンフィグをすべてファイルにエクスポートすることができますが、今度は膨大なコンフィグファイルの量で逆にわからなくなります。

今回はそんなcreate-react-appから卒業するために各種ツールチェインの設定を1から丁寧に解説していこうと思います。

今回の構築する開発環境

Webpack

複数のソースコードを1ファイルにまとめる(バンドルと言います)バンドラーと呼ばれるツールです。バンドルする際に各種ツールにソースコードを通したり画像のファイルパスを自動的に解決してくれたりと万能です。「モダンなWebフロント開発=バンドラーを使った開発」 とも言えるでしょう。

TypeScript

JavaScriptに安全性をもたらす上位互換な言語です。JavaScriptは他言語だとエラーになるようなことも当たり前のようにエラーにならなかったりするので型やnull安全性をもたらしてくれるTypeScriptはほぼ必須の存在です。

Babel

新しいJS/TSの文法を古い文法に変換してくれるツールです。Webフロントでは実行環境がブラウザに依存するため様々なブラウザ・バージョンをサポートしなければなりません。新しいJSの文法が使いたいのに古いブラウザへのサポートもしたい…!そんなときにはぜひともBabelを使いましょう。

SCSS

CSSに変数やfor文、if文を追加したCSSの上位互換な言語です。他にもスタイルをネストして書けたりするので快適にCSSを書くことができます。

PostCSS

CSSに対して色々な処理を行うことができるツールです。今回は自動的にベンダープレフィックスを付けてくれるautoprefixerとCSSのコードサイズを縮小するcssnanoを導入します。

※ESLintやPrettierも導入したいのですが、そこまで解説するととんでもない文字数になってしまいそうなので今回は見送らせていただきます。

環境構築

Node.js

はじめにNode.jsプロジェクトのセットアップを行います。今回はnpmではなくyarnを用いて解説します。npmでもpnpmでもお好きなパッケージマネージャをお使いください。

yarn init -y

package.jsonが作成されます。デフォルトで問題ありませんが、必要に応じて変更してください。

TypeScript

yarn add -D typescript
# TypeScriptのヘルパーライブラリです。
yarn add tslib

次にtsconfig.jsonを作成します。古いバージョンへのトランスパイルはbabelに任せるので、TypeScriptで出力するコードはesnextにします。

tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "jsx": "react",
    "sourceMap": true,
    "removeComments": true,
    "importHelpers": true,
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Webpackの初期設定

Webpackの設定ファイルはJavaScriptで記述することが多いですが、どうせなら型による安全性を設定ファイルにも導入しましょう。いくつかのTypeScriptのパッケージを導入してWebpackの設定を行います。

yarn add -D webpack webpack-cli @types/webpack ts-node @types/node

webpack.config.tsを作成する前に、tsconfig.jsonに少し手を加えます。ts-nodeを用いて実行したときのみ反映される設定を追加します。

tsconfig.json
{
  "compilerOptions": {
    // ...
  },
  "ts-node": {
    "compilerOptions": {
      "target": "es2020",
      "module": "commonjs",
      "lib": ["es2020"]
    }
  }
}

webpack.config.tsを作成します。loaderやpluginの設定は後回しにして、ベースとなる設定のみをここで行います。

src/index.tsxをエントリーポイントにして、dist/ディレクトリを出力とします(お好みで変更してください)。
Webpackにはmodeという概念があり、production modeとdevelopment modeの2種類がありますが、今回はNODE_ENVという環境変数を読み込んで判断することにします(未設定の場合はdevelopment modeになります。)。同様に、ベースとなるパス関してもBASE_URLという環境変数を読み込んで設定します(未設定の場合は/になります。)

webpack.config.ts
import { Configuration } from "webpack";
import * as path from "path";

const isProduction = process.env.NODE_ENV === "production";
const isDevelopment = !isProduction;

const baseURL = process.env.BASE_URL ?? "/";

const config: Configuration = {
  target: "web",
  mode: isProduction ? "production" : "development",
  entry: {
    index: path.join(__dirname, "src", "index.tsx"),
  },
  output: {
    path: path.join(__dirname, "dist"),
    publicPath: baseURL,
    filename: "assets/scripts/[name].[contenthash:8].js",
    chunkFilename: "assets/scripts/chunk.[contenthash:8].js",
  },
};

export default config;

まだloaderを一つも設定していないのでsrc/index.tsxというファイルを作って実行してもバンドルに失敗します。

Babel

Babelでは過去の文法への変換は行ってくれますが、最新の標準ライブラリの補完は行ってくれません。そこでcore-jsも導入します。

yarn add -D @babel/core @babel/preset-env
yarn add core-js

Babelの設定ファイルである.babelrcを作成します。

.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

Babelでは.browserslistrcというファイルを読み込んでどのバージョンの文法へトランスパイルするかを判断する機能があります。基本的にデフォルト設定で構いませんが、「IE許すまじ」の意志を込めてIEをサポートから外しておきます。

.browserslistrc
defaults
not ie 11

具体的にどんなブラウザをサポートする設定になっているかは、以下のコマンドで確認することができます。必要に応じてサポートするブラウザやバージョンを変更しましょう。

npx browserslist

loaderの設定

とりあえずTypeScriptとBabelの設定が行えたのでWebpackに各種loaderを設定します。
TypeScriptとBabelのloaderを導入します。

yarn add -D ts-loader babel-loader

webpack.config.tsにloaderの設定を追加します。

webpack.config.ts
const config: Configuration = {
  // ...
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  // ...
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
          },
          {
            loader: "ts-loader",
          },
        ],
      },
    ],
  },
  // ...
};

Reactの導入

yarn add react react-dom
yarn add -D @types/react @types/react-dom

ここまでの動作確認も含めていくつかファイルを作って実際に動作させてみます。

src/index.tsx
import React, { StrictMode } from "react";
import { render } from "react-dom";

import App from "./App";

render(
  <StrictMode>
    <App />
  </StrictMode>,
  document.getElementById("root"),
);
src/App.tsx
import React, { FC } from "react";

const App: FC = () => (
  <div>
    <h1>Hello, world!</h1>
  </div>
);

export default App;
package.json
{
  // ...
  "scripts": {
    "build": "webpack"
  }
  // ...
}
yarn build

dist/下にファイルが出力されたら成功です。

SCSS

SCSSのプロセッサーにはC++製のnode-sassとDart製のdart-sassの2種類がありますが、公式の推奨がdart-sassのため、こちらを導入します。

yarn add -D sass fibers @types/sass @types/fibers style-loader css-loader sass-loader

webpack.config.tsにCSS系のloader設定を追加します。CSS Moduleを使用したいため、[name].module.(css|sass|scss)だった場合はCSS Moduleを有効にするようにします。

webpack.config.ts
// ...
import sass from "sass";
import fibers from "fibers";

const config: Configuration = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(?:c|sa|sc)ss$/,
        use: [
          {
            loader: "style-loader",
          },
          {
            loader: "css-loader",
            options: {
              sourceMap: isDevelopment,
              importLoaders: 1,
              modules: {
                auto: true,
                localIdentName: isProduction ? "[hash:base64:8]" : "[path][name]__[local]",
                exportLocalsConvention: "dashesOnly",
              },
            },
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: isDevelopment,
              implementation: sass,
              sassOptions: {
                fiber: fibers,
              },
            },
          },
        ],
      },
      // ...
    ],
  },
  // ...
};

このままではTypeScriptのコンパイル時に、「CSS/Sass/SCSSを読み込むことができない」というエラーが発生してしまうのでsrc/global.d.tsを作成して読み込めるようにします。

src/global.d.ts
declare module "*.module.css" {
  const classes: { readonly [key: string]: string };
  export default classes;
}
declare module "*.module.scss" {
  const classes: { readonly [key: string]: string };
  export default classes;
}
declare module "*.module.sass" {
  const classes: { readonly [key: string]: string };
  export default classes;
}

動作確認のためにSCSSのファイルを作成します。

src/App.module.scss
.title {
  font-size: 3rem;
  color: lightblue;
}

src/App.tsxを変更してSCSSを適用させます。

src/App.tsx
import React, { FC } from "react";

import styles from "./App.module.scss";

const App: FC = () => (
  <div>
    <h1 className={styles.title}>Hello, world!</h1>
  </div>
);

export default App;

yarn buildを実行してエラーが発生しなければ成功です。

PostCSS

今回はPostCSSのプラグインとしてautoprefixerとcssnanoを導入します。

yarn add -D postcss autoprefixer cssnano postcss-loader

.postcssrcを作成します。

.postcssrc
{
  "plugins": ["autoprefixer", "cssnano"]
}

webpack.config.tsにPostCSSのloaderを追加します。

webpack.config.ts
const config: Configuration = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(?:c|sa|sc)ss$/,
        use: [
          // ...
          {
            loader: "css-loader",
            options: {
              // ...
              importLoaders: 2,
              // ...
            },
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: isDevelopment,
            },
          },
          {
            loader: "sass-loader",
            // ...
          },
        ],
      },
      // ...
    ],
  },
  // ...
};

プラグインの導入

Webpackのプラグインを導入します。今回は、バンドルしたJavaScriptをHTMLタグとして注入してくれるhtml-webpack-pluginとWebpackの出力先にファイルをそのままコピーするcopy-webpack-pluginを導入します。

yarn add -D html-webpack-plugin copy-webpack-plugin @types/html-webpack-plugin @types/copy-webpack-plugin

webpack.config.tsにプラグインの設定を追加します。

webpack.config.ts
const config: Configuration = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      inject: "head",
      minify: isProduction,
      template: path.join(__dirname, "src", "index.html"),
      scriptLoading: "defer",
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.join(__dirname, "public"),
        },
      ],
    }),
  ],
  // ...
};

html-webpack-pluginで用いるHTMLを作成します。

src/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Hello, world!</title>
  </head>
  <body>
    <noscript>
      <strong>This site uses JavaScript. Please enable it.</strong>
    </noscript>
    <div id="root"></div>
  </body>
</html>

copy-webpack-pluginの動作確認のためにpublic/ディレクトリを作成し、robots.txtを作成します。

public/robots.txt
user-agent: *

yarn buildを実行して動作確認をします。

開発用のローカルサーバーのセットアップ

webpack-dev-serverを導入します。ホットリロード付きなので開発中の動作確認が楽になります。

yarn add -D webpack-dev-server @types/webpack-dev-server

webpack.config.tsに設定を追加します。

webpack.config.ts
const config: Configuration = {
  // ...
  devServer: {
    historyApiFallback: true,
  },
  // ...
};

package.jsonにscriptを新しく追加します。

package.json
{
  // ...
  "scripts": {
    // ...
    "dev": "webpack serve"
  }
  // ...
}

yarn devを実行してWebページがきちんと表示されれば成功です。

ソースマップの設定

ソースマップがあると開発時のデバッグがやりやすくなります。

webpack.config.ts
const config: Configuration = {
  // ...
  devtool: isDevelopment ? "eval-source-map" : "nosources-source-map",
  // ...
};

これで終了です。お疲れさまでした。

おわり

最後に何度も書き換わったpackage.jsonwebpack.config.tsの全体像を示しておきます。

package.json
{
  "scripts": {
    "build": "webpack",
    "dev": "webpack serve"
  },
  "dependencies": {
    "core-js": "^3.8.1",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "tslib": "^2.0.3"
  },
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/preset-env": "^7.12.7",
    "@types/copy-webpack-plugin": "^6.3.0",
    "@types/fibers": "^3.1.0",
    "@types/html-webpack-plugin": "^3.2.4",
    "@types/node": "^14.14.10",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@types/sass": "^1.16.0",
    "@types/webpack": "^4.41.25",
    "@types/webpack-dev-server": "^3.11.1",
    "autoprefixer": "^10.0.4",
    "babel-loader": "^8.2.2",
    "copy-webpack-plugin": "^6.3.2",
    "css-loader": "^5.0.1",
    "cssnano": "^4.1.10",
    "fibers": "^5.0.0",
    "html-webpack-plugin": "^4.5.0",
    "postcss": "^8.1.14",
    "postcss-loader": "^4.1.0",
    "sass": "^1.30.0",
    "sass-loader": "^10.1.0",
    "style-loader": "^2.0.0",
    "ts-loader": "^8.0.11",
    "ts-node": "^9.1.0",
    "typescript": "^4.1.2",
    "webpack": "^5.10.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
  }
}
webpack.config.json
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import CopyWebpackPlugin from "copy-webpack-plugin";
import sass from "sass";
import fibers from "fibers";
import * as path from "path";

const isProduction = process.env.NODE_ENV === "production";
const isDevelopment = !isProduction;

const baseURL = process.env.BASE_URL ?? "/";

const config: Configuration = {
  target: "web",
  mode: isProduction ? "production" : "development",
  entry: {
    index: path.join(__dirname, "src", "index.tsx"),
  },
  output: {
    path: path.join(__dirname, "dist"),
    publicPath: baseURL,
    filename: "assets/scripts/[name].[contenthash:8].js",
    chunkFilename: "assets/scripts/chunk.[contenthash:8].js",
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
          },
          {
            loader: "ts-loader",
          },
        ],
      },
      {
        test: /\.(?:c|sa|sc)ss$/,
        use: [
          {
            loader: "style-loader",
          },
          {
            loader: "css-loader",
            options: {
              sourceMap: isDevelopment,
              importLoaders: 2,
              modules: {
                auto: true,
                localIdentName: isProduction ? "[hash:base64:8]" : "[path][name]__[local]",
                exportLocalsConvention: "dashesOnly",
              },
            },
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: isDevelopment,
            },
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: isDevelopment,
              implementation: sass,
              sassOptions: {
                fiber: fibers,
              },
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      inject: "head",
      minify: isProduction,
      template: path.join(__dirname, "src", "index.html"),
      scriptLoading: "defer",
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.join(__dirname, "public"),
        },
      ],
    }),
  ],
  devServer: {
    historyApiFallback: true,
  },
  devtool: isDevelopment ? "eval-source-map" : "nosources-source-map",
};

export default config;

最後に

明日は @Siketyan による「『限界開発鯖』を支える技術」です。私の所属している限界開発鯖の雰囲気がわかる(?)と思うのでぜひご覧ください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-dndのimportで起こった事

react-dndのimport時にエラーが出た

「react-dnd」を利用してサンプルを作ろうと思った時に「import」でエラーが出たので対処方を書こうと思います。
大したものではないです。

import { DragDropContext } from 'react-dnd'がない

import { DragDropContext } from "react-dnd"

DragDropContextが見つからないと言われてしまいます。
最新ではこれが変わっていて…

import { DndProvider } from 'react-dnd';

になっています。

import ReactDnDHTML5Backend from 'react-dnd-html5-backend'がない

import ReactDnDHTML5Backend from "react-dnd-html5-backend"

ReactDnDHTML5Backendが見つからないと言われてしまいます。
最新ではこれが変わっていて…

import { HTML5Backend } from 'react-dnd-html5-backend';

になっています。


調べても意外とスンナリ見つからなかったので、参考になれば嬉しいです。
ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React HooksでWebSocket通信を行うサンプル

はじめに

WebSocketを使ってリアルタイム通信アプリを作ろうとした場合、React Hooksなら「はいはい、副作用はuseEffect使って書けばいいんでしょ」というのはわかるものの、いざ書くとなるとこに何を書けばいいやら案外迷うものです。
本記事では、簡単なチャットアプリを題材にその方法をまとめました。useEffectフックや、カスタムフックの使いどころについて少しでも参考になれば幸いです。

なお、WebSocketのライブラリはsocket.ioを用いています。

動作検証環境:

  • Mac OS Catalina 10.15.7
  • react 17.0.1
  • socket.io 3.0.3
  • express 4.17.1

サンプルアプリケーション概要

名前を入れて入室後、メッセージを送信あるいは他のユーザーが送信したメッセージを受信するという簡単なチャットアプリです。

chatapp_001.png

サーバー側の実装

socket.ioライブラリを使ってWebSocket通信を行う簡単なプログラムを作成し、Expressにホストします。

app.js
const app = require('express')();
const server = require('http').createServer(app);
const io = require('socket.io')(server);

io.on('connection', (socket) => {
    console.log('A client connected.');
    socket.on('send', (payload) => {
        console.log(payload);
        socket.broadcast.emit('broadcast', payload);
    });
    socket.on('disconnect', () => {
        console.log('Conenction closed.');
    });
});

server.listen(3001, () => {
    console.log('Listening..');
});

簡単な解説:

  • send イベントを受信すると、他のクライアントに対してブロードキャストを行います。
  • 3001番ポートで待ち受けます。

React Hooksアプリの実装

まず、3000番ポートで立ち上げたdev-serverで動くReactアプリから、3001番ポートで待ち受けるサーバーアプリとそのまま通信しようとするとクロスオリジンのエラーとなるため、クライアント側の package.json を編集してプロキシ設定を行っています。

"proxy": "http://localhost:3001",

チャットコンポーネントの実装の概略です(イベントハンドリングや、WebSocket通信を除く部分)。

Chat.js
const Chat = ({name}) => {
    const [messages, setMessages] = useState([{
        name: '管理人', text: `ようこそ、${name}さん`
    }]);

    const [text, setText] = useState('');

    //(途中略)

    return (
        <div>
            <div className="input">
                <input type="text" placeholder="メッセージ" value={text} onChange={handleInputChange} />
                <button disabled={!text} onClick={handleButtonClick}>送信</button>
            </div>
            <ul>
                {
                    messages.map((msg, idx) => {
                        return (
                            <Message key={idx} name={msg.name} text={msg.text} />
                        );
                    })
                }
            </ul>
        </div>
    );
}

useState フックを使って定義したステート変数 messages には、入室時の初期メッセージ、自分が送信したメッセージ、他のユーザーから受信したメッセージが時系列に格納された配列が入ります。
JSXではその配列を map 関数で変換して箇条書きリストに表示しています。

さて、ここからが本題のWebSocket通信です。

Chat.js
    const socketRef = useRef();

    useEffect(() => {
        console.log('Connectinng..');
        socketRef.current = io();
        socketRef.current.on('broadcast', payload => {
            console.log('Recieved: ' + payload);
            setMessages(prevMessages => [...prevMessages, payload]);
        });
        return () => {
            console.log('Disconnecting..');
            socketRef.current.disconnect();
        };
    }, []);

まず、 socket.io-clientの io 関数により作られるソケットオブジェクトをどこに宣言するべきかという問題があります。
ソケット接続はコンポーネントのマウント時に一度だけ行いたいため、 useEffect の第1引数に渡す関数内で初期化を行いますが、このオブジェクトは他の場所からも参照したいため、 useEffect の外側のスコープで定義する必要があります。
初めの頃、私は以下のように関数の外側に定義をしていました。

let socket;

const Chat = ({name}) => {

しかし、このような参照の保持にはまさに useRef フックが使えるということに気づきました。

    const socketRef = useRef();

さきほど「ソケット接続はコンポーネントのマウント時に一度だけ行いたい」と書きましたが、これを実現するためには、 useEffect 関数の第2引数の依存配列に空配列を指定します。
この第2引数の配列に含まれるプロパティやステート変数の値が変更されると、Reactがコンポーネントの再レンダリングを行う際に useEffect の第1引数の関数を再実行します。そのため、空配列を指定すると(値変更の監視対象変数が存在しないため)最初の一度だけ実行されるという挙動になるのです。

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

さて、ソケット接続を最初に一度だけ行いたいのと同じように、接続のクローズも最後に一度だけ行いたいです。(ここで「最後」とは、コンポーネントがアンマウントされる時点を指します)。
これは、 useEffect の第1引数の関数の戻り値として後始末用の関数を返すことが実現可能です。

        return () => {
            console.log('Disconnecting..');
            socketRef.current.disconnect();
        }

サンプルアプリでは、退室ボタンをクリックすると Chat コンポーネントがレンダリング対象から外れアンマウントされるので、そのタイミングでコンソールのログに Disconnecting.. というメッセージが出るのを確認できます。

App.js
  return (
    <div>
      <Operation onEnter={handleEnter} onLeave={handleLeave} entered={entered} />
      { entered && <Chat name={name} />}
    </div>
  )

(上記コードで、 entered の値が偽のとき、 Chat コンポーネントはレンダリング対象から外れる)

useEffect の第1引数の関数では、ソケットオブジェクトを生成後、サーバーからの broadcast イベントを受信した際のコールバック関数を登録します。コールバック関数では、受信したメッセージをステート変数で管理されるメッセージ配列へ追加します(正確に言うと、新しいメッセージ配列をステート変数に格納)。

Chat.js
        socketRef.current = io();
        socketRef.current.on('broadcast', payload => {
            console.log('Recieved: ' + payload);
            setMessages(prevMessages => [...prevMessages, payload]);
        });

一方、送信ボタンクリックで呼び出されるイベントハンドラでは、新しいメッセージを作成して send イベントをサーバーに送出するとともに、ステート変数へも格納します。

Chat.js
    const handleButtonClick = (e) => {
        const aMessage = {
            name: name,
            text: text,
        };
        socketRef.current.emit('send', aMessage);
        setMessages(prevMessages => [...prevMessages, aMessage]);
        setText('');
    }

Chat コンポーネントのソース全量は以下のとおりです。

Chat.js
import {useState, useEffect, useRef} from 'react';
import Message from './Message';
import {io} from 'socket.io-client';

const Chat = ({name}) => {
    const [messages, setMessages] = useState([{
        name: '管理人', text: `ようこそ、${name}さん`
    }]);

    const [text, setText] = useState('');

    const socketRef = useRef();

    useEffect(() => {
        console.log('Connectinng..');
        socketRef.current = io();
        socketRef.current.on('broadcast', payload => {
            console.log('Recieved: ' + payload);
            setMessages(prevMessages => [...prevMessages, payload]);
        });
        return () => {
            console.log('Disconnecting..');
            socketRef.current.disconnect();
        };
    }, []);

    const handleInputChange = (e) => {
        setText(e.target.value);
    };

    const handleButtonClick = (e) => {
        const aMessage = {
            name: name,
            text: text,
        };
        socketRef.current.emit('send', aMessage);
        setMessages(prevMessages => [...prevMessages, aMessage]);
        setText('');
    }

    return (
        <div>
            <div className="input">
                <input type="text" placeholder="メッセージ" value={text} onChange={handleInputChange} />
                <button disabled={!text} onClick={handleButtonClick}>送信</button>
            </div>
            <ul>
                {
                    messages.map((msg, idx) => {
                        return (
                            <Message key={idx} name={msg.name} text={msg.text} />
                        );
                    })
                }
            </ul>
        </div>
    );
}

export default Chat;

カスタムフックを用いたリファクタリング

前述のChat コンポーネントには socket.io-client ライブラリを使った WebSocket 通信処理を記述していますが、これには以下の問題があります。

  • 特定の実装ライブラリへの依存が発生していること
  • 責務の分離ができていないこと

とくに、 useEffect 内ではなく、イベントハンドラ関数の内部にソケットを使った副作用コードが書かれているのは気持ち悪いです。

    const handleButtonClick = (e) => {
        const aMessage = {
            name: name,
            text: text,
        };
        socketRef.current.emit('send', aMessage);
        setMessages(prevMessages => [...prevMessages, aMessage]);
        setText('');

このような場合、カスタムフックを使えば簡単に処理を抽出してコードをリファクタリングすることが可能です。
関連するソースコードをごそっと別ファイルにコピーして手を加え、以下のようなカスタムフック関数を作成します。

useChatService.js
import {useState, useEffect, useRef} from 'react';
import {io} from 'socket.io-client';

const useChatService = (initialMessage) => {
    const [messages, setMessages] = useState([initialMessage]);

    const socketRef = useRef();

    useEffect(() => {
        console.log('Connectinng..');
        socketRef.current = io();
        socketRef.current.on('broadcast', payload => {
            console.log('Recieved: ' + payload);
            setMessages(prevMessages => [...prevMessages, payload]);
        });
        return () => {
            console.log('Disconnecting..');
            socketRef.current.disconnect();
        };
    }, []);

    const sendMessage = (name, text) => {
        const aMessage = {
            name: name,
            text: text,
        };
        socketRef.current.emit('send', aMessage);
        setMessages(prevMessages => [...prevMessages, aMessage]);
    }

    return [messages, sendMessage];
}

export default useChatService;

Chat コンポーネントから利用したい、メッセージの配列とメッセージ送信関数を戻り値として返却します。

Chat コンポーネントは以下のようにすっきりしました。

Chat2.js
import useChatService from './useChatService';

const Chat2 = ({name}) => {
    const [messages, sendMessage] = useChatService({
        name: '管理人', text: `ようこそ、${name}さん`
    });

    const [text, setText] = useState('');

    const handleInputChange = (e) => {
        setText(e.target.value);
    };

    const handleButtonClick = (e) => {
        sendMessage(name, text);
        setText('');
    }
    // 以下変更なし  

おわりに

サンプルソースコードは GitHub にアップしました。
サンプルの動作方法はそちらの README.md を参照ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新しいマシンにVue.js Reactの環境をインストールする方法

自分用に新しいマシンにVue.jsやReactをインストールする方法をメモとして残す
マニュアルインストールの方法は別にまとめる(予定)

Macに『HomeBrew』をインストールする

  • 何故インストールするのか?
    • CUIベースで環境構築がしやすくなる
    • 『$ brew install xxx』でCUIツールをインストールできる
    • 『$ brew cask install xxx』でGUIツールをインストールできる

HomeBrewインストール手順

  • 1. HomeBrewのHPを開く
  • 2. Terminalを開く
  • 3. インストールコマンドを実行する
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

brewコマンドでMacの開発環境を構築していく

  • Google Chrome
$ brew cask install google-chrome
  • VSCode
$ brew cask install visual-studio-code
  • IntelliJ
    • IntelliJのHPを開く (Ultimate版は有料なので、適宜検討する)
https://www.jetbrains.com/ja-jp/idea/download/#section=mac

Node.jsをインストールする

  • 1. Terminalを開く
  • 2. 下記コマンドを実行する (この時は最新のnodeのバージョンがインストールされる)
$ brew install node
  • 3. インストールされたNodeのバージョンを確認する
$ node -v
ex). v12.14.1
  • 4. npmコマンドが実行できるか確認する (試しにバージョン情報を確認する)
$ npm -v
ex). 6.14.8
  • 5. nodeのバージョン管理コマンドをインストールする (今回は『n』を使用する)
$ npm install -g n
  • 6. nコマンドが実行できるか確認する (試しにバージョン情報を確認する)
$ n -V
ex). 6.7.0

nについて

$ n latest
  • インストール済みのNodeのバージョンを確認する
$ n ls
  • Nodeのバージョンを切り替える
    • 実行後にインストール済みのNodeのバージョンが表示されるので変更したいバージョンを選択する
$ n

Vue.jsの環境構築方法

vue/cliコマンドを実行できるようにする

  • 1. @vue/cliのHPを開く
  • 2. Terminalを開く
  • 3. @vue/cliをインストールする
$ npm install -g @vue/cli
  • ※1 バージョンを指定してインストールする場合は『@』をつける (Version 4.5.7をインストールする場合)
$ npm install -g @vue/cli@4.5.7
  • ※2 updateの仕方 (updateするときもinstallを実行することにより最新バージョンを取得できる)
$ npm install -g @vue/cli
  1. インストール済みの@vue/cliのバージョンを確認する
$ npm -g ls | grep @vue/cli
ex). @vue/cli@4.5.7

Vueのプロジェクトを作成する

  • 1. Terminalを開く
  • 2. プロジェクトを新規に作成する (今回はプロジェクト名を『sample』とする)
$ vue create sample

Reactの環境構築方法

create-react-appコマンドを実行できるようにする

  • 1. create-react-appのHPを開く
  • 2. Terminalを開く
  • 3. create-react-appをインストールする
$ npm install -g create-react-app
  • ※1 バージョンを指定してインストールする場合は『@』をつける (Version 3.4.1をインストール)
$ npm install -g create-react-app@3.4.1
  • ※2 updateの仕方 (updateするときもinstallを実行することにより最新バージョンを取得できる)
$ npm install -g create-react-app
  • 4. インストール済みのcreate-react-appのバージョンを確認する
$ npm -g ls | grep create-react-app
ex). create-react-app@3.4.1

Reactのプロジェクトを作成する

  • 1. Terminalを開く
  • 2. プロジェクトを新規に作成する (今回はプロジェクト名を『sample』とする)
$ create-react-app sample
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

チェックボックスのアクセシビリティ対応をしたお話

この記事はフラー Advent Calendar 2020 の 7日目の記事です。
6日目は、aikosogabeさんで、フラーに入社して1ヶ月経ったでした。

最近、仕事でWebアクセシビリティについて調べる機会があったので、今回はその中でもつい最近調べた、チェックボックスのアクセシビリティについてやったことを書いていこうと思います。

前提

仕事で画像のような、クリックするとチェックされるようなカスタムチェックボックスをReactで作っていました。

スクリーンショット 2020-12-06 17.40.38.png
スクリーンショット 2020-12-06 17.40.36.png

で、その時作成したコードはだいたいこんな感じでした。
(TypeScriptを使用しているのでtsx形式になっていますが、見る上ではjsxだと思っていただけると......。)

Checkbox.tsx
const Checkbox = () => {
  const [isChecked, setIsChecked] = useState(false);

  const handleChange = () => {
    setIsChecked(!isChecked);
  };

  return (
    <div className="content">
      <label className={`${isChecked? "checkbox cheked" : "checkbox"}`}>
        <input type="checkbox" checked={isChecked} onChange={handleChange} />
        <span>スイッチ</span>
      </label>
    </div>
  );
};
checkbox.css
.content {
  width: 100px;
}
.checkbox {
  display: block;
  border: 2px solid grey;
  text-align: center;
}

.checked {
  background: yellow;
}

input {
  display: none;
}

コードをぱっとみても分からない人向けに説明をすると、CheckboxはHTMLを返す関数です。
isCheckedでチェックボックスの状態を管理して、チェックボックスが押されたタイミングで状態を反転させるようにしています。(handleChange)

さて、このチェックボックスにアクセシビリティ対応をすることになったのですが、一言でWebアクセシビリティと言っても「キーボードナビゲーション」や「スクリーンリーダー」など、様々な要素があります。この記事では「スクリーンリーダー」への対応に関して書いていきます。

また、これから紹介する結果は以下のOS、ブラウザで確認しています。
OS: macOS Catalina バージョン 10.15.7
ブラウザ: Chrome バージョン 87.0.4280.67
スクリーンリーダー: VoiceOver(PC)

対応策

display:noneを使用しない

前提の項目で紹介したデモのコードですが、スクリーンリーダーではうまく動作しません。
手元の環境ではspan要素内の文字を読むことまでは確認できましたが、この要素がチェックボックスであることまではスクリーンリーダーで読み取ることができません。

スクリーンショット 2020-12-06 22.17.51.png

これはinput要素に display:none が設定されているためです。
ある要素に対して display:nonevisibility:hidden が設定されていると、スクリーンリーダーのような支援技術はその要素を認識しなくなります。
そのため、前提の項目で例示したデモのコードでは、input要素そのものがスクリーンリーダーから認識されないので、チェックボックスであるというinput要素そのものが持つ情報も抜け落ちてしまいます。

参考: コンテンツの非表示と更新

display:none はもともと、チェックボックスの見た目をカスタマイズするときにinput要素を見えないようにするためのものです。
なので display:none を消しただけでは「チェックボックスをカスタマイズする」という目的は達成できません。
以下の画像のようにinput要素が見えてしまいます。
スクリーンショット 2020-12-06 22.37.11.png

opacity: 0でinput要素を視覚的に見えないようにする

次はinput要素をどうやって視覚的に消すかですが、調べた限りではいくつか方法があると思います。
例えば、 clip: rect(0px, 0px, 0px, 0px)で可視領域を狭める手法です。
参考: chakra Checkbox

なので、ここで使用した手法はあくまでいくつかある実装の一つだと思っています。

ここで使用した手法は opacity: 0 で視覚的に見えなくするという方法です。
変更はcssだけなのでcssだけ載せます。

checkbox.css
.content {
  width: 100px;
}
.checkbox {
  position: relative; /* 追加 */
  display: block;
  border: 2px solid grey;
  text-align: center;
}

.checked {
  background: yellow;
}

input {
  position: absolute; /* 追加 */
  margin: 0; /* 追加 */
  width: 100%; /* 追加 */
  height: 100%; /* 追加 */
  opacity: 0; /* 追加 */
}

opacityの他にもいくつかcssを追加しています。
positionはinputの領域をlabelの領域に重ねるため、widthheightはスクリーンリーダーのカーソルを当てた時の領域を見た目通りにするためです。

以下の画像はcss変更後のチェックボックスですが、前提の項目で紹介したデモとは違い、スクリーンリーダーにチェックボックスと認識されていることが分かります。
スクリーンショット 2020-12-06 23.13.16.png

あとはデザインを少し修正して、対応完了です。
スクリーンショット 2020-12-06 23.18.32.png

まとめ

今回はスクリーンリーダーにやさしいチェックボックスの一例として自分がやったことをまとめました。
普段はあまり意識しない場所かと思いますが、スクリーンリーダーのことも考えてみるきっかけになればと思います。
それはそれとして、自分でも引き続きWebアクセシビリティについて勉強していこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む