Closed32

Reactでじゃんけんアプリをつくる

Reactチュートリアル終わったので、実際にちょっとしたモノをつくってみる

Herokuで動かせればいいかな
Vercelとかも興味あるんだけど

設計図

じゃんけんって英語で「Rock paper scissors」らしい。ちょっと長いけど、レポジトリ名これにするか

npmで生成するプロジェクト名は大文字含まれていたらダメらしい

Cannot create a project named "RockPaperScissors" because of npm naming restrictions:

  * name can no longer contain capital letters

Please choose a different project name.

Herokuの使い方、Railsチュートリアルで言われるがままにやったので改めて整理する

Herokuが入ってるか確認
$ heroku --version
入ってなければ入れる
$ brew tap heroku/brew && brew install heroku
$ source <(curl -sL https://cdn.learnenough.com/heroku_install) # AWS Cloud9の場合
Herokuへのログイン
$ heroku login
$ heroku login --interactive # ブラウザでやりたくない場合
Herokuで新たにアプリをつくる
$ heroku create
Herokuで新たにアプリをつくる
$ heroku create
Herokuにリポジトリをpushする
$ git push heroku main

役に立つことあるかわからんけど、Herokuにpushしたときのログ

$ git push heroku main
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 4 threads
Compressing objects: 100% (16/16), done.
Writing objects: 100% (16/16), 187.15 KiB | 8.14 MiB/s, done.
Total 16 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Building on the Heroku-20 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> Node.js app detected
remote:        
remote: -----> Creating runtime environment
remote:        
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_VERBOSE=false
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        
remote: -----> Installing binaries
remote:        engines.node (package.json):  unspecified
remote:        engines.npm (package.json):   unspecified (use default)
remote:        
remote:        Resolving node version 14.x...
remote:        Downloading and installing node 14.17.0...
remote:        Using default npm version: 6.14.13
remote:        
remote: -----> Installing dependencies
remote:        Installing node modules
remote:        
remote:        > core-js-pure@3.12.1 postinstall /tmp/build_357b4c07/node_modules/core-js-pure
remote:        > node -e "try{require('./postinstall')}catch(e){}"
remote:        
remote:        
remote:        > core-js@2.6.12 postinstall /tmp/build_357b4c07/node_modules/babel-runtime/node_modules/core-js
remote:        > node -e "try{require('./postinstall')}catch(e){}"
remote:        
remote:        
remote:        > ejs@2.7.4 postinstall /tmp/build_357b4c07/node_modules/ejs
remote:        > node ./postinstall.js
remote:        
remote:        
remote:        > fsevents@1.2.13 install /tmp/build_357b4c07/node_modules/webpack-dev-server/node_modules/fsevents
remote:        > node install.js
remote:        
remote:        
remote:        Skipping 'fsevents' build as platform linux is not supported
remote:        
remote:        > fsevents@1.2.13 install /tmp/build_357b4c07/node_modules/watchpack-chokidar2/node_modules/fsevents
remote:        > node install.js
remote:        
remote:        
remote:        Skipping 'fsevents' build as platform linux is not supported
remote:        
remote:        > core-js@3.12.1 postinstall /tmp/build_357b4c07/node_modules/core-js
remote:        > node -e "try{require('./postinstall')}catch(e){}"
remote:        
remote:        added 1958 packages in 25.019s
remote:        
remote: -----> Build
remote:        Running build
remote:        
remote:        > rock-paper-scissors@0.1.0 build /tmp/build_357b4c07
remote:        > react-scripts build
remote:        
remote:        Creating an optimized production build...
remote:        Compiled successfully.
remote:        
remote:        File sizes after gzip:
remote:        
remote:          42.18 KB  build/static/js/2.c999cfcd.chunk.js
remote:          1 KB      build/static/js/main.575f9350.chunk.js
remote:          782 B     build/static/js/runtime-main.959750e8.js
remote:          384 B     build/static/css/main.27870d5d.chunk.css
remote:        
remote:        The project was built assuming it is hosted at /.
remote:        You can control this with the homepage field in your package.json.
remote:        
remote:        The build folder is ready to be deployed.
remote:        You may serve it with a static server:
remote:        
remote:          npm install -g serve
remote:          serve -s build
remote:        
remote:        Find out more about deployment here:
remote:        
remote:          https://cra.link/deployment
remote:        
remote:        
remote: -----> Caching build
remote:        - node_modules
remote:        
remote: -----> Pruning devDependencies
remote:        audited 1958 packages in 14.134s
remote:        
remote:        136 packages are looking for funding
remote:          run `npm fund` for details
remote:        
remote:        found 79 moderate severity vulnerabilities
remote:          run `npm audit fix` to fix them, or `npm audit` for details
remote:        
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> web
remote: 
remote: -----> Compressing...
remote:        Done: 69.3M
remote: -----> Launching...
remote:        Released v3
remote:        https://thawing-fjord-72897.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.

Reactでアニメーションを調べると、色々大袈裟なものが出てくるけれど、
よく考えたら、今回やりたいのは相手が出すグー、チョキ、パーを切り替えるだけなので、単なるタイマー処理なのか

アニメーション処理は不要で、stateの更新をすることでアニメっぽくできた

class Round extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      playerHand: "",
      opponentHand: "✊",
    };
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.change(),
      100
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  change() {
    console.log(this.state.opponentHand);
    if (this.state.opponentHand === '✊') {
      this.setState({
        opponentHand: "✌️"
      });
    } else if (this.state.opponentHand === "✌️") {
      this.setState({
        opponentHand: "✋"
      });
    } else if (this.state.opponentHand === "✋") {
      this.setState({
        opponentHand: "✊"
      });
    }
  }

  render() {
    const opponentHand = this.state.opponentHand;
    return (
      <div className="round">
        <div className="opponent-hand">
        {opponentHand}
        </div>
        <div className="message">
          何を出しますか?
        </div>
        <div className="player-hand">
        ✊✌️✋
        </div>
      </div>
    );
  }
}

今日はこんなもんかな
明日はプレイヤーの出した手のボタンコンポーネントをつくる

  • プレイヤーの出した手のボタンをつくる
  • じゃんけんの勝敗判定ロジックをつくる
  • 勝敗を結果画面に反映する(stateにフラグが要るかな?)
  • 結果画面をタップすると最初に戻る

今更ながら、<div>要素は水平方向のグルーピングをしているので、囲んだ要素はネストしなければ水平に並ぶ

setStateを使ったら、こんなエラーが出た。

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

そのときのコード(抜粋)

  setPlayerHand(hand) {
    this.setState({ playerHand: hand });
  }

  render() {
    return (
        <div className="player-hand">
          <PlayerHand
            value="✊"
            onClick={this.setPlayerHand("✊")}
          />

パーの絵文字には✋と🖐がある。
(指が微妙に開いてる)
ロジックの中で絵文字直比較でやってるから、たまに間違える。
本当はあんま良くないな。
SwiftだったらEnumで縛るんだけど……

↑のアロー関数の使いどころもそうだけど、thisのつけ忘れもよく怒られる。
あと{}が必要か不要かも雰囲気でやってる

子コンポーネントから親コンポーネントのメソッド呼ばせたら、これで怒られた

TypeError: Cannot read property 'setState' of undefined

↑これはbindして解決した

このやり方をしたら、引数がsyntheticBaseEventになってしまった

function PlayerHand(props) {
  return (
    <button
      className="player-hand"
      onClick={props.onClick}
    >
      {props.value}
    </button>
  );
}

class Page extends React.Component {
  renderPlayerHand(hand) {
    return <PlayerHand
            value={hand}
            onClick={this.props.onClick(hand)}
          />
  }

  render() {
    return (
      <div className="app">
        <OpponentHand value={this.props.opponentHand}/>
        <div className="message">
          {calculateWinner(this.props.playerHand, this.props.opponentHand)}
        </div>
        <div className="player-hand">
          {this.renderPlayerHand("✊")}
          {this.renderPlayerHand("✌️")}
          {this.renderPlayerHand("✋")}
        </div>
      </div>
    );
  }
}

// ↓親コンポーネントのこれを呼んでいる
  setPlayerHand(hand) {
    this.setState({ playerHand: hand });
  }

すげーハマった。
3時間ぐらい考えて調べたけど、これでできた。

function OpponentHand(props) {
  return (
    <div className="opponent-hand">
      {props.value}
    </div>
  );
}

function PlayerHand(props) {
  return (
    <button
      className="player-hand"
      onClick={props.onClick}
    >
      {props.value}
    </button>
  );
}

class Page extends React.Component {
  renderPlayerHand(hand) {
    return <PlayerHand
            value={hand}
            onClick={() => this.props.onClick(hand)}
          />
  }

  render() {
    return (
      <div className="app">
        <OpponentHand value={this.props.opponentHand}/>
        <div className="message">
          {calculateWinner(this.props.playerHand, this.props.opponentHand)}
        </div>
        <div className="player-hand">
          {this.renderPlayerHand("✊")}
          {this.renderPlayerHand("✌️")}
          {this.renderPlayerHand("✋")}
        </div>
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      playerHand: "",
      opponentHand: "✊",
    };
    this.setPlayerHand = this.setPlayerHand.bind(this);
    this.clearPlayerHand = this.clearPlayerHand.bind(this);
  }

  componentDidMount() {
    this.setTimer()
  }

  setTimer() {
    this.timerID = setInterval(
      () => this.changeOpponentHand(),
      500
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  changeOpponentHand() {
    if (this.state.playerHand) { 
      clearInterval(this.timerID); 
    }

    if (this.state.opponentHand === '✊') {
      this.setState({
        opponentHand: "✌️"
      });
    } else if (this.state.opponentHand === "✌️") {
      this.setState({
        opponentHand: "✋"
      });
    } else if (this.state.opponentHand === "✋") {
      this.setState({
        opponentHand: "✊"
      });
    }
  }

  setPlayerHand(hand) {
    console.log(hand);
    this.setState({ playerHand: hand });
  }

  clearPlayerHand() {
    this.setState({ playerHand: "" });
  }

  render() {
    const playerHand = this.state.playerHand;
    const opponentHand = this.state.opponentHand;
    if(playerHand) {
      return (
        <div className="app" onClick={this.clearPlayerHand}>
          <Page 
            playerHand={playerHand}  
            opponentHand={opponentHand}
            onClick={(hand) => this.setPlayerHand(hand)}
          />
        </div>
      );
    } else {
      return (
        <div className="app">
          <Page 
            playerHand={playerHand}  
            opponentHand={opponentHand}
            onClick={(hand) => this.setPlayerHand(hand)}
          />
        </div>
      );
    }
  }
}

アロー関数で渡すと実行されなくて、そのまま渡すと即時実行になるらしく、ひたすらアロー関数でパスしていくのがお作法みたい。

↑結局チュートリアルのコードを見直したら、できた。

タイマーのリセットタイミングの問題だったので、修正。

このスクラップは2021/05/13にクローズされました
ログインするとコメントできます