20200206のReactに関する記事は3件です。

Reactチュートリアルプログラム

目的

Reactの学習のため、下記のチュートリアルプログラムを作成してみる。
https://ja.reactjs.org/tutorial/tutorial.html

環境

Windows10Home
node v10.16.0
npm 6.9.0

記述

create-react-app

テキストエディタを使用するので、ベースとしてcreate-react-appを使用。
下記を実行し土台を作成。

npm
npx create-react-app test
cd test
npm start

その後作成したtest\src配下のファイルを全て削除し、index.css,index.jsを作成。

cssの記述

index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

Reactの記述

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(<Game />, document.getElementById("root"));

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

追加課題

1.履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

 historyにマスの情報を追加し、値を呼び出せるように変更。

index.js(handleClick)
handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares,
          col: (i % 3) + 1, //追加
          row: Math.floor(i / 3) + 1, //追加
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

遷移リストにマスの位置を表示するようにstep.col,step.row追加

index.js(render)
  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move +'('+ step.col +','+ step.row +')' : //変更
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。

GameコンポーネントstepNumberをステートとして保持しているので、これを利用してstepNumberhistoryのindexの役割をするmoveが一致した際にboldというclassを付与

index.js(render)
  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move +'('+ step.col +','+ step.row +')' :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}
          className={this.state.stepNumber=== move ? 'bold':'' } >{desc}</button>
        </li>
      );
    });

boldclass付与時に太字が有効になるようにcssに追加

index.css
.bold {
  font-weight: bold;
}

3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

keyを追加し、map関数を二重で使用する。

index.js(Board)
class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
        key={i}
      />
    );
  }

  render() {
    return (
      <div>
        {
          Array(3).fill(0).map((row, i) => {
            return (
              <div className="board-row" key={i}>
                {
                  Array(3).fill(0).map((col, j) => {
                    return(
                      this.renderSquare(i * 3 + j)
                    )
                  })
                }
              </div>
            )
          })
        }
      </div>
    );
  }
}

4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

Gameコンポーネントに昇順・降順を並べ替えるボタンを追加し、昇順か降順かどうかを保持するstateを新しく定義することで遷移リストをレンダリングする際にstateの状態によって昇順か降順を切り替えを行う。

index.js(Game)
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares,
          col: (i % 3) + 1,
          row: Math.floor(i / 3) + 1,
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
      isAsc: true
    });
  }

  toggleAsc() {
     this.setState({
       asc: !this.state.asc,
     });
   }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move +'('+ step.col +','+ step.row +')' :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}
          className={this.state.stepNumber=== move ? 'bold':'' } >{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <div><button onClick={() => this.toggleAsc()}>{(this.state.asc ? "desc":"asc")}</button></div>
          <ol>{this.state.asc ? moves : moves.reverse()}</ol>
        </div>
      </div>
    );
  }
}

5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。

calculateWinner関数に勝者の判定だけでなくどのマスの列が揃ったかの配列も返すように変更を加える。

index.js(calculateWinner)
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return{
        winner: squares[a],
        line: [a,b,c]
      };
    }
  }
  return null;
}

calculateWinner関数の帰り値を変更したため、Gameコンポーネントの修正とBoardコンポーネントに揃った列のマスを渡すように変更。

index.js(Game)
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares,
          col: (i % 3) + 1,
          row: Math.floor(i / 3) + 1,
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
      isAsc: true
    });
  }

  toggleAsc() {
     this.setState({
       asc: !this.state.asc,
     });
   }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const settlement = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move +'('+ step.col +','+ step.row +')' :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}
          className={this.state.stepNumber=== move ? 'bold':'' } >{desc}</button>
        </li>
      );
    });

    let status;
    if (settlement) {
      status = "Winner: " + settlement.winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
            highlightCells={settlement ? settlement.line : []}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <div><button onClick={() => this.toggleAsc()}>{(this.state.asc ? "desc":"asc")}</button></div>
          <ol>{this.state.asc ? moves : moves.reverse()}</ol>
        </div>
      </div>
    );
  }
}

Boardコンポーネントに揃った列のマスの配列を受け取り、マスをレンダリング時に揃ったマスをハイライトするか否をisHighlightで判定。

index.js(Board)
class Board extends React.Component {
  renderSquare(i, isHighlight = false) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
        key={i}
        isHighlight={isHighlight}
      />
    );
  }

  render() {
    return (
      <div>
        {
          Array(3).fill(0).map((row, i) => {
            return (
              <div className="board-row" key={i}>
                {
                  Array(3).fill(0).map((col, j) => {
                    return(
                      this.renderSquare(i * 3 + j, this.props.highlightCells.indexOf(i * 3 + j) !== -1)
                    )
                  })
                }
              </div>
            )
          })
        }
      </div>
    );
  }
}

SquareコンポーネントisHighlighttrueのときのみhighlightクラスを付与するように変更。

index.js(Square)
function Square(props) {
  return (
    <button 
    className={`square ${props.isHighlight ? 'highlight' : ''}`} 
    onClick={() => props.onClick()}>
      {props.value}
    </button>
  );
}

最後にhighlightクラスがついたときに背景色が変わるようにcssに追加。

index.css
.highlight {
  background-color: yellow;
}

6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

calculateWinner関数に引き分けかどうかの情報も渡すように変更。

index.js(calculateWinner
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return{
        winner: squares[a],
        line: [a,b,c],
        isDraw: false
      };
    }
  }
  if(squares.filter((e) => !e).length === 0){
    return {
      isDraw: true,
      winner: null,
      line: []
    }
  }
  return null;
}

Gameコンポーネントrenderメソッドを書き換えていきます。

index.js(Game)
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares,
          col: (i % 3) + 1,
          row: Math.floor(i / 3) + 1,
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
      isAsc: true
    });
  }

  toggleAsc() {
     this.setState({
       asc: !this.state.asc,
     });
   }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const settlement = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move +'('+ step.col +','+ step.row +')' :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}
          className={this.state.stepNumber=== move ? 'bold':'' } >{desc}</button>
        </li>
      );
    });

    let status;
    if (settlement) {
      if (settlement.isDraw) {
        status = "Draw";
      }else{
        status = "Winner: " + settlement.winner;
      }
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
            highlightCells={settlement ? settlement.line : []}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <div><button onClick={() => this.toggleAsc()}>{(this.state.asc ? "desc":"asc")}</button></div>
          <ol>{this.state.asc ? moves : moves.reverse()}</ol>
        </div>
      </div>
    );
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + Create React Appで環境変数を使いたかった(自戒)

失敗談と解決方法です
やらかしたメモ

実現させたかったこと

Azure上のApp Serviceで設定した環境変数をデプロイしたDockerイメージで使用したかった。
CRA(Create React App)を使用しているため、.envファイルにREACT_APP_と先頭とつけて書いておくと環境変数として読み込まれる。。
 → App Serviceから環境変数として渡されている値をもとに.envを生成する。

試した実装と望んでいた動作

DockerfileのCMDを使用してDocker runのタイミングで.envを生成するシェルスクリプトを実行する。
↓↓↓

CMD [ "/bin/bash", "-c", "./env.sh" ]
env.sh
#!/bin/bash

rm -rf .env
touch .env

while read -r line || [[ -n "$line" ]];
do
  if printf '%s\n' "$line" | grep -q -e '='; then
    varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
    varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
  fi

  value=$(printf '%s\n' "${!varname}")
  [[ -z $value ]] && value=${varvalue}

  echo "$varname = \"$value\"" >> .env
done < .env.key
.env.key
REACT_APP_ID = 
REACT_APP_PW = 

.env.keyにある変数名をもとに.envを生成して、下の要領でアプリケーション内で読み込みたかった。

id = process.env.REACT_APP_ID;
pw = process.env.REACT_APP_PW;

原因

.envの読み込みタイミングはDockerfile内のyarn buildが実行されたとき。
yarn buildが実行されているのはdocker buildをした際のみで、.envが生成される前。
docker runが実行されるのもApp Serviceから環境変数が渡されるのもdocker buildが終了し、App Serviceにデプロイされたタイミング。
 → .envの生成自体は成功していたが読み込まれていなかった。

ただのミスだった…

解決策

下記のようにenvファイルを変更して、環境変数を保持するjsファイルを生成する。
index.htmlheadでスクリプトとして読み込ませることで、window._env_.***として呼び出すことができる。

呼び出し
id = window._env_.REACT_APP_ID; 
pw = window._env_.REACT_APP_PW; 
env.sh
#!/bin/bash

rm -rf ./env-config.js
touch ./env-config.js

echo "window._env_ = {" >> ./env-config.js

while read -r line || [[ -n "$line" ]];
do
  if printf '%s\n' "$line" | grep -q -e '='; then
    varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
    varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
  fi

  value=$(printf '%s\n' "${!varname}")
  [[ -z $value ]] && value=${varvalue}

  echo "  $varname: \"$value\"," >> ./env-config.js
done < .env

echo "}" >> ./env-config.js
index.html
<head>
...
    <script src="%PUBLIC_URL%/env-config.js"></script>
...
</head>

参考

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

highlight.js で一部の言語のみに対応して webpack のビルドサイズを減らす方法

こんにちは、フロントエンジニアの ku6ryo です。
コード文字列をハイライトしてくれる highlight.js を使う機会があったのですが、highlight.js のライブラリ全体のファイルサイズが大きくて webpack ビルドのサイズを大きくしていたので必要な言語のみ対応して、ビルドサイズを大幅に削減する方法を紹介します。

まず、highlight.js の大きさは webpack で build した場合、Gzip しないとサイズが 746.69 KB、Gzip すると 246.18 KB のサイズがあります。

Screen Shot 2020-02-06 at 1.46.50.png

いずれにしても、とても大きなサイズなことは代わりありませんね(汗)
すべての対応言語を読み込んでしまうと、このようなサイズになります。
必要な言語をのみを読み込むには、以下のように必要な言語のファイルのみを読み込む設定ファイルを作ります。例えば、Javascript, CSS, JSON のみに対応するには、以下のようなファイルを作ります。

// my-highlight.js
import hljs from 'highlight.js/lib/highlight'

hljs.registerLanguage('css', require('highlight.js/lib/languages/css'))
hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'))
hljs.registerLanguage('json', require('highlight.js/lib/languages/json'))

export default hljs

そして、このファイルを highlight.js を呼ぶときと同じように使えば OK です。

// 通常
import hljs from 'highlihgt.js'

// 必要なものだけ読み込む
import hljs from './my-highlight.js'

上記の言語セットの場合、Gzip 前 19KB、Gzip 後 7.14 KB になります。Gzip 前 700 KB 以上、Gzip 後 200 KB 以上のサイズ削減になります。これはだいぶ大きですよね! highlight.js 使っている方はぜひやってみてくださいね〜

Screen Shot 2020-02-06 at 2.01.41.png

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