Reactでじゃんけんアプリをつくる
Reactチュートリアル終わったので、実際にちょっとしたモノをつくってみる
Herokuで動かせればいいかな
Vercelとかも興味あるんだけど
ローカルにHeroku入ってなかったので入れる
設計図
じゃんけんって英語で「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.
公開するほどのリポジトリになるとも思えないが、非公開にする理由もないのでpublicにした
ここでやっていく
Herokuの使い方、Railsチュートリアルで言われるがままにやったので改めて整理する
$ heroku --version
$ brew tap heroku/brew && brew install heroku
$ source <(curl -sL https://cdn.learnenough.com/heroku_install) # AWS Cloud9の場合
$ heroku login
$ heroku login --interactive # ブラウザでやりたくない場合
$ heroku create
$ heroku create
$ 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>要素は水平方向のグルーピングをしているので、囲んだ要素はネストしなければ水平に並ぶ
タイマー処理が気にあったので調べた。
node.jsがサポートしている仕組みっぽい
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("✊")}
/>
onClick={this.setPlayerHand("✊")}
この書き方だと即時実行になってしまい、無限ループに陥る。
onClick={() => this.setPlayerHand("✊")}
これなら意図していた挙動。
パーの絵文字には✋と🖐がある。
(指が微妙に開いてる)
ロジックの中で絵文字直比較でやってるから、たまに間違える。
本当はあんま良くないな。
SwiftだったらEnumで縛るんだけど……
↑のアロー関数の使いどころもそうだけど、this
のつけ忘れもよく怒られる。
あと{}
が必要か不要かも雰囲気でやってる
子コンポーネントから親コンポーネントのメソッド呼ばせたら、これで怒られた
TypeError: Cannot read property 'setState' of undefined
this
をbindしてないのが問題っぽい……
↑これは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 });
}
この辺か
SyntheticEvent
の扱いがよくわからない……
やっぱりわからない
すげーハマった。
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>
);
}
}
}
アロー関数で渡すと実行されなくて、そのまま渡すと即時実行になるらしく、ひたすらアロー関数でパスしていくのがお作法みたい。
↑結局チュートリアルのコードを見直したら、できた。
とりあえずここまでで当初思ってた挙動にはなった。
ただ止めたときに一回スベる?
onClickが二回走ってるっぽい
タイマーのリセットタイミングの問題だったので、修正。
CSSを整えて、完成。
なお余白については、margin
とpadding
で指定できた
完成!