5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React + Material-UI】数字当てゲームを作ろう!【2020年4月版】

Last updated at Posted at 2020-04-05

はじめに

この記事は**「Reactのチュートリアルを終えて、何かを作ってみたい」**という読者を想定しています。
分からない部分が出た場合はReactの公式ドキュメントに立ち返りましょう。

material-uiの公式ドキュメントも要チェック

前にReact+Material-UIで作ったもの

今回作ったもの

React Number Guess
ランダムに生成された1~100の数字を推測するゲームです。「60より上ですか?」などの質問をしていき、数字が絞れたら「〇〇ですか?」で正解を出します。右側に予想履歴が表示されます。
スクリーンショット 2020-04-05 13.52.31.png

Githubのソースコード

戦略

ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します
また、今回はuseEffectを使い、読み込み時に1~100のランダムな数字を生成します。

component, containerの洗い出し

component

  • 数字ボタン: 0~9の数字を入力できるボタンコンポーネント。色はピンクで固定。
スクリーンショット 2020-04-05 17.58.53.png
  • 大きいボタン: 「より下ですか?」「クリア」「予想する」ボタンなど、数字ボタン以外に使うコンポーネント。色をpropsで指定できる。
スクリーンショット 2020-04-05 17.59.07.png
  • ディスプレイ: 現在の質問を表示するコンポーネント
スクリーンショット 2020-04-05 17.59.53.png
  • 予想履歴: いままでの予想の一覧を表示するコンポーネント
スクリーンショット 2020-04-05 18.00.34.png
  • 正解モーダル: 正解したときに表示されるモーダルコンポーネント
スクリーンショット 2020-04-05 18.26.58.png

その他、タイトルと説明はコンポーネントにせず、containerにそのまま書きます。

container

  • NumberGuess: すべてのコンポーネントを集めて表示するコンテナ

stateの洗い出し

  1. inputNumber: 予想に使う数字(int)
  2. questionContent: 質問の内容。"より下ですか?"、"より下ですか?"、"ですか?"の3種類(string)
  3. history: 質問の履歴が格納された配列(array)。 [[34,"より下ですか?", "はい"],[17,"より下ですか?", "はい"],[7,"より上ですか?", "はい"]] のような2次元配列
  4. answer: 答えの数字。はじめにuseEffectでランダムに設定される(int)
  5. open: 正解したときにmodalを開くために必要なboolean
  6. guessTimes: 質問した回数(int)

インストール

create react app

npx create-react-app react-calculator
cd react-calculator

@material-ui/core

// npm を使う場合
npm install @material-ui/core

// yarn を使う場合
yarn add @material-ui/core

バージョン情報

  • react: 16.13.1
  • @material-ui/core: 4.9.9

ファイル構成

スクリーンショット 2020-04-05 18.28.24.png

コンポーネントの実装

Display

Display.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

const useStyles = makeStyles((theme) => ({
    box: {
        width: "100%",
        margin: "10px auto",
    },
  }));

export default function Display(props) {
  const classes = useStyles();

  return (
      <div>
       <Box className={classes.box}>
           <Typography variant="h3">
                {props.children}
           </Typography>
       </Box>
      </div>
  );
}

NumberButton

NumberButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function NumberButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(1),
      backgroundColor: "#f48fb1",
    },
  }));
  const classes = useStyles();

  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}

ポイント

  • propsでonClickイベントを受け取って、ButtonのonClickに接続しています

LongButton

LongButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function LongButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(2),
      padding: theme.spacing(1),
      backgroundColor: props.backgroundColor,
    },
  }));
  const classes = useStyles();

  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}

ポイント

  • 数字ボタンと他のボタンでスタイルを変えようと思いましたが、同じような見た目になってしまいました。こちらは背景色をpropsに渡すことができます。

GuessHistory

GuessHistory.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';

const useStyles = makeStyles((theme) => ({
    box: {
        textAlign: "left",
        margin:"20px 0",
        padding:"0 20px"
    },
  }));

export default function GuessHistory(props) {
  const classes = useStyles();

  const history = props.history;

  return (
      <div>
        <Typography>予想履歴</Typography>
        <Box className={classes.box}>
          {
            history.map((guess, index) => 
            <Box key={index} marginBottom="10px">
              <Typography variant="h5">
                {index+1}.  {guess[0]}{guess[1]}:  {guess[2]}
              </Typography>
            </Box>
            )
          }
        </Box>
      </div>
  );
}

###ポイント

  • historypropsで受け渡しています。history.mapで単一のguessを受け取り、Box内で表示しています。
  • mapを使うときはコンポーネントにkeyをつけるのを忘れずに。

正解モーダル

SuccessModal.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';
import Backdrop from '@material-ui/core/Backdrop';
import Fade from '@material-ui/core/Fade';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

const useStyles = makeStyles((theme) => ({
  modal: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  paper: {
    backgroundColor: theme.palette.background.paper,
    border: '2px solid #000',
    boxShadow: theme.shadows[5],
    padding: theme.spacing(2, 4, 3),
  },
}));

export default function TransitionsModal(props) {
  const classes = useStyles();
  const guessTimes = props.guessTimes;
  const answer = props.answer;

  const open = props.open
  
  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        className={classes.modal}
        open={open}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <div className={classes.paper} >
              <Box marginBottom="30px" id="transition-modal-title">
                <Typography  variant="h4">正解です!</Typography>
              </Box>
              <Box  id="transition-modal-description">
                <Typography variant="h5">正解の数字: {answer}</Typography>
                <Typography variant="h5">正解までの質問数: {guessTimes}</Typography>
              </Box>
          </div>
        </Fade>
      </Modal>
    </div>
  );
}

ポイント

  • 元となるコンポーネントはmaterial-uiの公式ドキュメント、モーダルから引っ張ってきました
  • propsopenを受け取ることで、コンテナのopenstateがtrueになったときに子のopenも更新されます。

コンテナの実装

NumberGuess.js
import React, {useState, useEffect} from 'react';
//components
import NumberButton from '../components/NumberButton';
import LongButton from '../components/LongButton';
import Display from '../components/Display';
import GuessHistory from '../components/GuessHistory';
import SuccessModal from '../components/SuccessModal';
//material-ui
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';

export default function NumberGuess() {
    //states
    const [inputNumber, setInputNumber] = useState(0);
    const [questionContent, setQuestionContent] = useState("");
    const [history, setHistory] = useState([]);
    const [answer,setAnswer] = useState(0);
    const [open, setOpen] = useState(false);
    const [guessTimes, setGuessTimes] = useState(0);
    
    //ページが読み込まれたときに一度だけ実行させる
    useEffect(() => {
        setAnswer(Math.floor( Math.random() * (100) ) + 1);
    }, [])

    //数字ボタンをクリックしたとき
    const handleNumberClick = (num) => {
        let newInputNumber = inputNumber*10 + num;
        if(newInputNumber >= 1 && newInputNumber <= 100){
            setInputNumber(newInputNumber);
        }
    }

    //予想の内容ボタンをクリックしたとき
    const handlequiestionContentClick = (content) => {
        if(content === "biggerThan"){
            setQuestionContent("より上ですか?");
        } else if(content === "smallerThan"){
            setQuestionContent("より下ですか?");
        } else if(content === "exact"){
            setQuestionContent("ですか?");
        }
    }

    //クリアボタンをクリックしたとき
    const handleClearClick = () => {
        setInputNumber(0);
        setQuestionContent("");
    }

    //予想ボタンをクリックしたとき
    const handleSubmitClick = () => {
        //予想の内容が入力されていない場合はすぐに返す
        if(questionContent === "") return;
        //予想した回数を1増やす
        setGuessTimes(guessTimes + 1);

        if(questionContent === "より上ですか?"){
            if(inputNumber < answer){
                setHistory([
                    ...history,
                    [inputNumber, "より上ですか?", "はい"]
                ]);
            } else {
                setHistory([
                    ...history,
                    [inputNumber, "より上ですか?", "いいえ"]
                ]);
            }
        } else if(questionContent === "より下ですか?"){
            if(inputNumber > answer){
                setHistory([
                    ...history,
                    [inputNumber, "より下ですか?", "はい"]
                ]);
            } else {
                setHistory([
                    ...history,
                    [inputNumber, "より下ですか?", "いいえ"]
                ]);
            }
        } else if(questionContent === "ですか?"){
            if(inputNumber === answer){
                setHistory([
                    ...history,
                    [inputNumber, "ですか?", "はい"]
                ]);
                //正解の場合にモーダルを開く
                setOpen(true);
            } else {
                setHistory([
                    ...history,
                    [inputNumber, "ですか?", "いいえ"]
                ]);
            }
        }
        setInputNumber(0);
        setQuestionContent("");
    }
  return (
      <div>
          <Box marginBottom="20px">
            <Typography variant="h4">React Number Guess</Typography>
            <Typography variant="h6">1~100の数字を予想しよう</Typography>
          </Box>
          <Grid container spacing={1}>
              <Grid item sm={6} xs={12}>
                <Display>{inputNumber} {questionContent}</Display>
                <Box display="flex" flexDirection="column">
                    <Box display="flex" justifyContent="center">
                        <NumberButton onClick={() => handleNumberClick(0)}>0</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(1)}>1</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(2)}>2</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(3)}>3</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(4)}>4</NumberButton>
                    </Box>
                    <Box display="flex" justifyContent="center">
                        <NumberButton onClick={() => handleNumberClick(5)}>5</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(6)}>6</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(7)}>7</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(8)}>8</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(9)}>9</NumberButton>
                    </Box>
                    <Box display="flex" justifyContent="center">
                        <LongButton onClick={() => handlequiestionContentClick("biggerThan")} backgroundColor="#f48fb1">より上ですか?</LongButton>
                        <LongButton onClick={() => handlequiestionContentClick("smallerThan")} backgroundColor="#f48fb1">より下ですか?</LongButton>
                        <LongButton onClick={() => handlequiestionContentClick("exact")} backgroundColor="#f48fb1">ですか?</LongButton>
                    </Box>
                    <Box display="flex" justifyContent="center">
                        <LongButton onClick={handleClearClick}  backgroundColor="#bdbdbd">クリア</LongButton>
                        <LongButton onClick={handleSubmitClick}  backgroundColor="#b2ff59">予想する</LongButton>
                    </Box>
                </Box>
              </Grid>
              <Grid item sm={6} xs={12}>
                <GuessHistory history={history}/>
              </Grid>
          </Grid>
          <SuccessModal open={open} guessTimes={guessTimes} answer={answer}/>
            
      </div>
  );
}

ポイント

  • useEffectを使って、マウント時に一度だけanswerを設定するようにしました。単純にconst answer = ...としていると、ボタンを押すごとに更新されてしまいます。
  • handleSubmitClicksetHistory内では元のhistoryを展開して、最後に新たな要素を追加しています。

App.js

App.js
import React from 'react';
import './App.css';

import NumberGuess from './containers/NumberGuess'

function App() {
  return (
    <div className="App">
      <div className="container">
        <NumberGuess />
      </div>
    </div>
  );
}

export default App;

App.css

App.css
.App {
  text-align: center;
}

.container{
  margin: 80px auto 80px auto;
  max-width: 900px;
  background-color: #80deea;
}

以上で完成です!

おまけ

こちらの記事を参考に、gh-pagesでサイトを公開しました。
https://shintaro-hirose.github.io/react-number-guess/

今後もReactで簡単なアプリを作っていきます!

5
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?