はじめに
この記事は**「Reactのチュートリアルを終えて、何かを作ってみたい」**という読者を想定しています。
分からない部分が出た場合はReactの公式ドキュメントに立ち返りましょう。
material-uiの公式ドキュメントも要チェック
前にReact+Material-UIで作ったもの
今回作ったもの
React Number Guess
ランダムに生成された1~100の数字を推測するゲームです。「60より上ですか?」などの質問をしていき、数字が絞れたら「〇〇ですか?」で正解を出します。右側に予想履歴が表示されます。
戦略
ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します
また、今回はuseEffectを使い、読み込み時に1~100のランダムな数字を生成します。
component, containerの洗い出し
component
- 数字ボタン: 0~9の数字を入力できるボタンコンポーネント。色はピンクで固定。
- 大きいボタン: 「より下ですか?」「クリア」「予想する」ボタンなど、数字ボタン以外に使うコンポーネント。色をpropsで指定できる。
- ディスプレイ: 現在の質問を表示するコンポーネント
- 予想履歴: いままでの予想の一覧を表示するコンポーネント
- 正解モーダル: 正解したときに表示されるモーダルコンポーネント
その他、タイトルと説明はコンポーネントにせず、containerにそのまま書きます。
container
- NumberGuess: すべてのコンポーネントを集めて表示するコンテナ
stateの洗い出し
- inputNumber: 予想に使う数字(int)
- questionContent: 質問の内容。"より下ですか?"、"より下ですか?"、"ですか?"の3種類(string)
- history: 質問の履歴が格納された配列(array)。
[[34,"より下ですか?", "はい"],[17,"より下ですか?", "はい"],[7,"より上ですか?", "はい"]]
のような2次元配列 - answer: 答えの数字。はじめにuseEffectでランダムに設定される(int)
- open: 正解したときにmodalを開くために必要なboolean
- 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
ファイル構成
コンポーネントの実装
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>
);
}
###ポイント
-
history
をprops
で受け渡しています。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の公式ドキュメント、モーダルから引っ張ってきました
-
props
でopen
を受け取ることで、コンテナのopen
stateが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 = ...
としていると、ボタンを押すごとに更新されてしまいます。 -
handleSubmitClick
のsetHistory
内では元の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で簡単なアプリを作っていきます!