20200405のReactに関する記事は5件です。

複数の GitHub リポジトリの情報を簡単に比較できるサービス「Repositories Comparison」を公開しました

こちらもご一緒にどうぞ
プログラミング言語別の求人数を一覧で見ることができるサービス「Nojov」を公開しました - Qiita

アプリケーション

Repositories Comparison

screenshot_table.png

作ったきっかけ

最近、業務で Golang を利用した新規開発のプロジェクトで利用するフレームワークの検討を行う機会がありました。
新規開発で利用するライブラリを選別する際に、 GitHub リポジトリの Star 数や最終コミットの日時等を比較することはよくあると思います。
Golang のフレームワークといえば Gin, Beego, echo, その他色々ありますが、それらのリポジトリを行ったり来たりして情報を見比べる作業がとても面倒でした。
そこで、複数のリポジトリの情報を簡単に見比べることができるサービスがあったら便利かもしれないと思い、作ってみました。

GitHub

使い方

アクセストークンを設定する

usage_1.png

Repositories Comparison では、リポジトリ情報の取得に GitHub API を利用しています。
アクセストークンを設定しなかった場合、 GitHub API は 1 時間あたり 60 件しかリクエストを送信できませんが、アクセストークンを設定することで 1 時間あたり 5000 件までリクエストを送信することができます。
Repositories Comparison を快適に利用するために、アクセストークンを設定することを推奨します。
GitHub アクセストークンはこちらから生成・取得してください。
アクセストークンに付与する必要がある最低限のスコープは public_repo です(プライベートリポジトリを指定する場合は repo が必要です)。

アクセストークンは localStorage に保存され、 GitHub API を利用したリポジトリ情報の取得以外の用途に使われることは一切ありません。

比較したいリポジトリを追加する

usage_2.png

入力フォームから比較したいリポジトリを追加していきます。
フォーマット: <owner>/<repo>

比較する

usage_3_1.png

後は COMPARE ボタンを押すだけです。
以下のようにリポジトリの情報を表形式で比較することができます。

usage_3_2.png

また、表形式だけでなくチャート形式で比較することもできます。

usage_3_3.png

利用技術

  • React
  • TypeScript
  • Docker
  • Terraform
  • Serverless Framework
  • AWS
    • Route 53
    • Certificate Manager
    • S3
    • CloudFront
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

はじめに

この記事は「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で簡単なアプリを作っていきます!

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

【React Hooks】stateの使用からみたクラスコンポーネントと関数コンポーネントのコード比較

これまでは、stateはクラスコンポーネントのみが持ち、関数コンポーネントの中では使用することができませんでした。
しかし、hookを導入することで関数コンポーネントの中でもstateが使えるようになりました。(hookはReact16.8で追加された機能です。)
今回は、stateを使用するクリックカウントコンポーネントを作成する場合の、クラスコンポーネントと関数コンポーネントのコードを比較をしていきたいと思います。

全体のコード

クラスコンポーネントの場合

Class.js
import React from 'react';

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

関数コンポーネントの場合

Function.js
import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

関数コンポーネントの方がコード量が少なく、見やすい印象を受けます。
それでは2つの違いを詳しく見ていきましょう。

1.stateの設定方法

クラスコンポーネントでは、コンストラクタ内でthis.stateにstate(count)を設定しています。

Class.js
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

関数コンポーネントでは、useStateフックを使用して、state(count)と、stateを更新する関数(setCount)を分割代入で受け取っています。
useStateの引数には初期値を渡します。

Function.js
import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

2.stateの読み出し

クラスコンポーネントで現在のcountの値を表示する場合、this.state.countとします。

Class.js
<p>You clicked {this.state.count} times</p>

関数コンポーネントでは、countとして直接呼び出せます。

Function.js
<p>You clicked {count} times</p>

3.stateの更新

クラスコンポーネントでは、this.setState()を使用します。

Class.js
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
  Click me
</button>

関数コンポーネントでは、useStateの分割代入で受け取ったsetCountを使用します。

Function.js
<button onClick={() => setCount(count + 1)}>
  Click me
</button>

まとめ

以上、クラスコンポーネントと関数コンポーネントをstateの使用から見て比較をしてみました。
関数コンポーネントの方が、stateの読み出しも更新も、「this」を介さずそのまま使用できるのですっきり見えます。
最後まで読んでいただきありがとうございました。

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

FigmaのプラグインのUIをReact+TSで作る

TL;DR

git clone https://github.com/kazuyaseki/react-figma-plugin-boilerplate.git <your project name>
yarn or npm install
yarn webpack:watch  or  npm run webpack:watch 

Figmaのプラグイン、テンションが上がるものなのですが、プラグイン自体がUIを必要とする & ある程度 Stateful なものな場合、命令的に作るのは耐えられない!という気持ちになりました。

なのでReactで開発できる boilerplate を作ってみたので、使い方をご紹介していこうと思います。

boilerplate の中身はこちらです。cloneしていい感じに使ってみてください。
https://github.com/kazuyaseki/react-figma-plugin-boilerplate

どうやって実現するか

特に難しいことは何もないんですが、Figmaのプラグインでは、プラグインが起動されたときのUIは manifest.jsonui のところに指定した ui.html を読み込むので、そこにエントリーポイントを用意してあげて ReactDOM.render するだけです。普通のReactアプリケーションですね。

ui.html
<div id="app"></div>
ui.tsx
import * as React from 'react';
import * as ReactDom from 'react-dom';

// App の中で普通にReact開発していく
import { App } from './App';

ReactDom.render(<App />, document.getElementById('app'));

開発中のプラグインをFigmaから使えるようにする

manifest.json へパスを通す必要があります。
Plugins > Development > New Plugin... をクリックすると次のようなWindowが出現するので、そこから指定してあげてください。
Untitled.png

サンプル

以上であとは boilerplate 試してみてくれ、というくらい語ることがないのですが、サンプルとして次のツイートのようなものを作ったのでコードを貼っておきます。

プラグイン作る上での注意点は、figma のデータを参照できるスレッドとプラグインのUI部分のスレッドは別物だというところです。詳しくはFigmaのドキュメントHow Plugins Runを参照ください。
なので message オブジェクトを使ってなんとかするのですが、下記サンプルにて main thread -> ui thread と ui thread -> main thread の両方向の例があるので参考にしてみてください。

code.tsx
import { subscribeOnMessages } from 'react-figma';

figma.showUI(__html__);

const componentNodes = figma.root.findAll((node) => node.type === 'COMPONENT');
// uiスレッドにそのままNode渡すとなぜかプロパティが undefined になるので成形してから渡す
const conmponentsData = componentNodes.map((node) => ({
  id: node.id,
  name: node.name,
}));
figma.ui.postMessage(conmponentsData);

figma.ui.onmessage = (message) => {
  subscribeOnMessages(message);

  if (message.type === 'create-instance') {
    const component = figma.root.findOne(
      (node) => node.id === message.id
    ) as ComponentNode;
    component.createInstance();
  }
};
App.tsx
import * as React from 'react';

type ComponentItemType = {
  id: string;
  name: string;
};

export const App = () => {
  const [query, setQuery] = React.useState('');
  const [components, setComponents] = React.useState<ComponentItemType[]>([]);

  React.useEffect(() => {
    onmessage = (event) => {
      setComponents(event.data.pluginMessage as ComponentItemType[]);
    };
  }, []);

  const create = (id: string) => {
    parent.postMessage({ pluginMessage: { type: 'create-instance', id } }, '*');
  };

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <div style={{ display: 'flex', flexDirection: 'column' }}>
        {components
          .filter((component) => {
            if (query.length === 0) {
              return true;
            }
            return component.name.includes(query);
          })
          .map((component) => (
            <button onClick={() => create(component.id)}>
              {component.name}
            </button>
          ))}
      </div>
    </div>
  );
};

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

【React + Material-UI】電卓アプリを作ろう!【2020年4月版】

はじめに

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

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

作ったもの

React Calculator

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

iPhoneに入っている電卓アプリのようなものを作りました。

Githubにソースコードを公開しています。

戦略

ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します。

component、containerの洗い出し

component

  • ボタン: 押したら何かしらの動作を起こすコンポーネント
    スクリーンショット 2020-04-05 0.20.00.png

  • ディスプレイ: 上部にある、数字を表示するためのコンポーネント

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

container

  • calculator: ボタンとディスプレイを集結させたもの。今回はこれ自体がアプリとなる。

stateの洗い出し

iPhoneの電卓アプリをいじってみて、どんなstateが必要かを考えます。はじめにざっくりと書き出して、実際には実装しながら追加していくカタチになります。

  1. value1: 演算対象の数字の文字列
  2. value2: value1に変化を与える数字の文字列
  3. operator: add(加算), subtract(減算), divide(除算), multiply(乗算)。operatorが指定されていないときはfalseにする。

  4. isAllClear: clearボタンがAC / Cどちらかを判別するboolean。

  5. isDicimalInput: .ボタンが押されて小数点以下入力になっているか判別するboolean。

  6. isAnswerDisplay: =ボタンが押された直後には特殊な挙動をするので、判別するbooleanを用意しておく。

たとえば、 5 * 7 =
と入力した場合、value1は5, value2は7, operatorは"multiply"である。

インストール

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.8

ファイル構成

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

コンポーネントの実装

Display

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

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

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

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

ポイント

props.childrenとすることで、<Display></Display>ではさんだものを表示できます。

MyButton

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

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


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

ポイント

ボタンコンポーネントは親から1. 背景色, 2. クリックしたときに実行する関数, 3. ボタンの文字を受け取ります。クリックしたときに実行する関数について、親コンポーネントで<MyButton onClick={somthing}>としても機能しないので、子のButtonのonClickに接続する必要があるんですね

コンテナの実装

長いコードなのでコメントアウトで説明していきます。

Calculator.js
import React, {useState} from 'react';

//components
import MyButton from '../components/MyButton';
import Display from '../components/Display';

//material-ui
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

function Calculator(){
    //保持したいstates
    const [value1, setValue1] = useState("0");
    const [value2, setValue2] = useState("0");
    const [operator, setOperator] = useState(false);
    const [isAllClear, setIsAllClear] = useState(true);
    const [isDicimalInput, setIsDicimalInput] = useState(false);
    const [isAnswerDisplay, setIsAnswerDisplay] = useState(false);

    //数字ボタンを押したとき
    const handleNumberClick = (num) => {
        //=ボタンを押した直後に数字を押した場合
        if(isAnswerDisplay){
            setValue1(num);
            setValue2("0");
            setOperator(false);
            setIsAnswerDisplay(false);
            setIsDicimalInput(false);
        } else {
           //演算子が押されていないときはvalue1を更新する
           if(!operator){
                if(isDicimalInput){
                    setValue1(String(value1) + String(num))
                } else {
                    setValue1(String(Number(value1)*10+num));
                }
            //演算子が押されているときはvalue2を更新する
            } else {
                if(isDicimalInput){
                    setValue2(String(value2) + String(num))
                } else {
                    setValue2(String(Number(value2)*10+num));
                }
            }
        }
        //0以外が押されたときはACをCに変える
        if(num !== 0){
            setIsAllClear(false);
        }
    }

    //00ボタンを押したとき
    const handleDoubleZeroClick = () => {
        if(!operator){
            if(isDicimalInput){
                setValue1(String(value1) + "00")
            } else {
                setValue1(String(Number(value1)*100));
            }
        } else {
            if(isDicimalInput){
                setValue2(String(value2) + "00")
            } else {
                setValue2(String(Number(value2)*100));
            }
        }
    }

    //AC/Cボタンを押したとき
    const handleClearClick = () => {
        setIsDicimalInput(false);
        //ACのときはすべてリセット
        if(isAllClear){
            setValue1("0");
            setOperator(false);
            setIsAnswerDisplay(false)
        //Cのときはvalue2を"0"にする
        } else {
            setIsAllClear(true);
            if(isAnswerDisplay){
                setValue1("0")
            } else{
                if(!operator){
                    setValue1("0");
                } else {
                    setValue2("0");
                }
            }
        }
    }

    // -/+ボタンを押したとき
    const handleMinusPlusClick = () => {
        if(isAnswerDisplay){
            setValue1(-1 * Number(value1));
        } else {
            if(!operator){
                setValue1(-1 * Number(value1));
            } else {
                setValue2(-1 * Number(value2));
            }
        }

    }

    // %ボタンを押したとき
    const handlePercentageClick = () => {
        if(isAnswerDisplay){
            setValue1(Number(value1) * 0.01);
        } else {
            if(!operator){
                setValue1(Number(value1) * 0.01);
            } else {
                setValue2(Number(value2) * 0.01);
            }
        }
    }

    //演算子を押したとき
    //引数はadd subtract divide multiplyのどれか
    const handleOperatorClick = (a) => {
        if(isAnswerDisplay){
            setIsAnswerDisplay(false);
        } else {
            if(value2 !== "0"){
                if(operator === "add"){
                    setValue1(Number(value1) + Number(value2))
                } else if(operator === "subtract"){
                    setValue1(Number(value1) - Number(value2))
                } else if(operator === "divide"){
                    setValue1(Number(value1) / Number(value2))
                } else if(operator === "multiply"){
                    setValue1(Number(value1) * Number(value2))
                }
            }
        }
        setIsDicimalInput(false);
        setOperator(a);
        setValue2("0")
    }

    // .ボタンを押したとき
    const handleDicimalPointClick = () => {
        if(isDicimalInput) return;

        setIsDicimalInput(true);
        if(!operator){
            setValue1(String(value1) + '.')
        } else {
            setValue2(String(value2) + '.')
        }
    }

    // =ボタンを押したとき
    const handleAnswerClick = () => {
        setIsDicimalInput(false);
        setIsAnswerDisplay(true);
        if(operator === "add") {
            if(value2 === "0"){
                //演算子が指定されたあと、value2が入力されずに=が押された場合
                //例えば、5 + = と入力した場合、10と表示する。
                setValue2(value1)
                setValue1(Number(value1) + Number(value1))
            } else {
                setValue1(Number(value1) + Number(value2))
            }
        } else if(operator === "subtract") {
            if(value2 === "0"){
                setValue2(Number(value1))
                setValue1(Number(value1) - Number(value1))
            } else {
                setValue1(Number(value1) - Number(value2))
            }
        } else if(operator === "divide") {
            if(value2 === "0"){
                setValue2(Number(value1))
                setValue1(Number(value1) / Number(value1))
            } else {
                setValue1(Number(value1) / Number(value2))
            }
        } else if(operator === "multiply"){
            if(value2 === "0"){
                setValue2(Number(value1))
                setValue1(Number(value1) * Number(value1))
            } else {
                setValue1(Number(value1) * Number(value2))
            }
        }
    }

    //ディスプレイに表示する内容
    const displayMarkup = isAnswerDisplay ? (
        value1
    ) : (
        operator ? (
            value2
        ) : (
            value1
        )
    );

    // AC/Cボタンの表示
    const clearButtonMarkup = isAllClear ? (
        "AC"
    ) : (
        "C"
    )
    return(
        <div>
            <Typography variant="h5">
                React Calculator
            </Typography>
            <Display>
                {displayMarkup}
            </Display>
            //material-uiのBoxはflexを使って好きなように並べられる。公式ドキュメント参照。
            <Box display="flex" flexDirection="column">
                <Box display="flex" justifyContent="center">
                   //ボタンの中に表示したいものをMyButtonではさむ
                   //MyButtonのpropsに背景色、onClick関数を渡す
                    <MyButton backgroundColor="gray" onClick={handleClearClick}>
                        <Typography variant="h5">{clearButtonMarkup}</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handleMinusPlusClick}>
                        <Typography variant="h5">-/+</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handlePercentageClick}>
                        <Typography variant="h5">%</Typography>
                    </MyButton>
                    //onClickに引数を渡したい場合はこのように記述する
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("divide")}>
                        <Typography variant="h5" >÷</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center" >
                    <MyButton onClick={() => handleNumberClick(7)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">7</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(8)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">8</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(9)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">9</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("multiply")}>
                        <Typography variant="h5" >×</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(4)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">4</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(5)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">5</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(6)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">6</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("subtract")}>
                        <Typography variant="h5" >-</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(1)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">1</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(2)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">2</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(3)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">3</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("add")}>
                        <Typography variant="h5" >+</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(0)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">0</Typography>
                    </MyButton>
                    <MyButton onClick={handleDoubleZeroClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5">00</Typography>
                    </MyButton>
                    <MyButton onClick={handleDicimalPointClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5" >.</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={handleAnswerClick}>
                        <Typography variant="h5" >=</Typography>
                    </MyButton>
                </Box>
            </Box>
        </div>
    )
};

export default Calculator;

ポイント

  • handleNumberClickなどの引数がある関数は、onClick={() => somthing(引数)}のように記述します
  • stateはひとつにまとめても良いが、どこで何を使うかが決まってないうちは独立したstateにしておいて後からまとめよう。

App.js

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

import Calculator from './containers/Calculator'

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

export default App;

App.css

App.css
.App {
  text-align: center;
}
.container {
  margin: 80px auto 80px auto;
  padding: 20px;
  max-width: 300px;
  background-color: #80deea;
}

以上で完成です!

おまけ

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

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

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