現在React.jsを勉強中でして、今回はこのような物を作成しました。
タイムアラームです。
サービスはFirebaseでホスティングしました。→URL(https://easy-alarm-cf7a0.web.app/)
ソースコードはindex.html
だた一枚(cssや音声ファイルは別ですが)。
リポジトリはこちら(https://github.com/smithshiro/easy-alarm)
React.jsのコンポーネントの組み合わせとCSSで時計の描画のみを使って作りました。
コンポーネントを分解
画面の要素は以下のようにコンポーネントとしてまとめました。
- 時計コンポーネント(Clock):時刻情報を管理して、時計盤やアラームに渡す役割
- 時計盤コンポーネント(ClockRound):時計盤を描画する役割
- 時針コンポーネント(HourHand):時針を描画する役割
- 分針コンポーネント(MinuteHand):分針を描画する役割
- 秒針コンポーネント(SecondHand):秒針を描画する役割
- アラームコンポーネント(Alarm):アラームを設定する役割
Clockコンポーネント
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// 時計コンポーネント class Clock extends React.Component { constructor(props) { super(props); this.state = { hour: 0, minute: 0, second: 0 } } // 1秒おきにステートを更新 componentDidMount() { let updateTime = () => { const date = new Date(); const hour = date.getHours(); const minute = date.getMinutes(); const second = date.getSeconds(); this.setState({ hour,minute,second }); }; updateTime(); setInterval(() => { updateTime(); }, 1000); } render() { return ( <div> <ClockRound hour={this.state.hour} minute={this.state.minute} second={this.state.second} /> <Alarm hour={this.state.hour} minute={this.state.minute} second={this.state.second} /> </div> ); } } |
componentDidMount内のsetIntervalで1秒おきに時刻を更新
ステートの更新はsetState
で行います。
更新した時刻のステート(hour
,minute
,second
)はClockRound
、Alarm
のプロパティとして渡しています。
ClockRoundコンポーネント
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
// 時計盤コンポーネント class ClockRound extends React.Component { constructor(props) { super(props); } // メモリの位置を設定 makeMemoryStyle(i) { let calcLeftPosition = function(i) { let modify = function(i) { switch (i) { case 9: return 0; case 8: case 10: return 0.29; case 7: case 11: return 0.55; case 0: case 6: return -1; case 1: case 5: return -4; case 2: case 4: return -3.5; case 3: return -16; } } return 100 * Math.cos(Math.PI / 2 - Math.PI / 6 * i) + modify(i) + 100; } let calcTopPosition = function(i) { let modify = function(i) { switch(i) { case 0: case 1: case 2: case 3: case 9: case 10: case 11: return 0; case 4: case 8: return -2; case 5: case 7: return -3; case 6: return -16; } } return -100 * Math.sin(Math.PI / 2 - Math.PI / 6 * i) + modify(i) + 100; } return { position: 'absolute', left: calcLeftPosition(i) + 'px', top: calcTopPosition(i) + 'px', width: (i === 3 || i === 9) ? 16 : 4 + 'px', height: (i === 0 || i === 6) ? 16 : 4 + 'px', background: '#fafafa' }; } makeMemoryDOM() { let dom = []; for (let i = 0; i < 12; i++) { const style = this.makeMemoryStyle(i); dom.push( <div key={i} style={style}></div> ); } return dom; } render() { const style = { margin: '50px auto 0px', position: 'relative', width: '200px', height: '200px', background: '#333', borderRadius: '200px' }; const memoryDOM = this.makeMemoryDOM(); return ( <div style={style}> {memoryDOM} <HourHand hour={this.props.hour} minute={this.props.minute} /> <MinuteHand minute={this.props.minute} /> <SecondHand second={this.props.second} /> </div> ); } } |
時刻のメモリの位置を計算して描画
時計盤の時刻のメモリの部分は三角関数で位置を調整して描画しました。
makeMemoryStyle
でその計算を行っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// メモリの位置を設定 makeMemoryStyle(i) { let calcLeftPosition = function(i) { let modify = function(i) { switch (i) { case 9: return 0; case 8: case 10: return 0.29; case 7: case 11: return 0.55; case 0: case 6: return -1; case 1: case 5: return -4; case 2: case 4: return -3.5; case 3: return -16; } } return 100 * Math.cos(Math.PI / 2 - Math.PI / 6 * i) + modify(i) + 100; } let calcTopPosition = function(i) { let modify = function(i) { switch(i) { case 0: case 1: case 2: case 3: case 9: case 10: case 11: return 0; case 4: case 8: return -2; case 5: case 7: return -3; case 6: return -16; } } return -100 * Math.sin(Math.PI / 2 - Math.PI / 6 * i) + modify(i) + 100; } return { position: 'absolute', left: calcLeftPosition(i) + 'px', top: calcTopPosition(i) + 'px', width: (i === 3 || i === 9) ? 16 : 4 + 'px', height: (i === 0 || i === 6) ? 16 : 4 + 'px', background: '#fafafa' }; } |
Clockコンポーネントの時刻の更新に合わせて針を動かす
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
// 時針コンポーネント class HourHand extends React.Component { constructor(props) { super(props); } render() { const center = {x: 100, y: 100}; const rotate = (this.props.hour + this.props.minute / 60) * 30; const style = { zIndex: '2', position: 'absolute', left: (center.x - 2) + 'px', top: (-65 + center.y) + 'px', width: '4px', height: '80px', background: '#7b7bff', transformOrigin: '50% 84%', transform: 'rotate(' + rotate + 'deg)' }; return ( <div style={style}></div> ); } } // 分針コンポーネント class MinuteHand extends React.Component { constructor(props) { super(props); } render() { const center = {x: 100, y: 100}; const rotate = this.props.minute * 6; const style = { zIndex: '1', position: 'absolute', left: (center.x - 2) + 'px', top: (-100 + center.y) + 'px', width: '4px', height: '117px', background: '#ff9393', transformOrigin: '50% 86%', transform: 'rotate(' + rotate + 'deg)' }; return ( <div style={style}></div> ); } } // 秒針コンポーネント class SecondHand extends React.Component { constructor(props) { super(props); } render() { const center = {x: 100, y:100}; const rotate = this.props.second * 6; const style = { zIndex: '3', position: 'absolute', left: (center.x - 0.5) + 'px', top: (-100 + center.y) + 'px', width: '1px', height: '120px', background: '#afffaf', transformOrigin: '50% 86%', transform: 'rotate(' + rotate + 'deg)' }; return ( <div style={style}></div> ); } } |
ClockコンポーネントのstateをClockRoundコンポーネント経由でそれぞれの針のコンポーネントであるHourHand,MinuteHand.SecondHandのprops
に渡しています。
それぞれのコンポーネントではprops
が更新されたタイミングでrender
関数が実行され、再度新しい時刻の値を元に針の座標が更新されます。
こうして、時刻通りに時計の針が動くようにしています。
設定した時刻通になるとアラームをあげるAlarmコンポーネント
最後に設定した時刻になればアラームをあげる(PCで見れば音がでます)Alarmコンポーネントについて説明します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
// アラームコンポーネント(時刻をセットする、アラームを出す) class Alarm extends React.Component { alermChecker = null; constructor(props) { super(props); this.state = { enableAlarm: false, alermTime: null, isAlarming: false, alermTime: '' } this.setAlarm = this.setAlarm.bind(this); this.clearAlarm = this.clearAlarm.bind(this); this.changeAlarmTime = this.changeAlarmTime.bind(this); this.stopAlarm = this.stopAlarm.bind(this); this.audio = new Audio('audio.mp3'); } isAlarm() { if (!this.state.enableAlarm) return false; if (this.state.isAlarming) return false; const alermTimeHour = this.state.alermTime.split(':')[0]; const alermTimeMinute = this.state.alermTime.split(':')[1]; if (alermTimeHour != this.props.hour) return false; if (alermTimeMinute != this.props.minute) return false; // 20秒以内ならアラームとする if (this.props.second > 20) return false; this.setState({isAlarming: true}); document.body.style = 'background: #ff9393'; return true; } setAlarm(e) { e.preventDefault(); this.setState({enableAlarm: true}); this.alermChecker = setInterval(() => { const result = this.isAlarm(); if(result) { this.audio.play(); } }, 1000); } clearAlarm(e) { e.preventDefault(); this.setState({ enableAlarm: false, alermTime: '', isAlarming: false }); document.body.style = 'background: #fafafa'; clearInterval(this.alermChecker); } changeAlarmTime(e) { this.setState({alermTime: e.target.value}); } stopAlarm(e) { e.preventDefault(); this.audio.pause(); this.clearAlarm(e); document.body.style = 'background: #fafafa'; } render() { const hour = ('0' + this.props.hour).substr(-2); const minute = ('0' + this.props.minute).substr(-2); const second = ('0' + this.props.second).substr(-2); let controllButton = () => { if(!this.state.isAlarming) { if (this.state.enableAlarm) { return ( <button onClick={this.clearAlarm} className='formButton clearAlarm'>解除</button> ); } else { return ( <button onClick={this.setAlarm} className='formButton'>設定</button> ); } } else { return ( <button onClick={this.stopAlarm} className='formButton stopAlarm'>停止</button> ); } } return ( <div> <div className='nowTime'>{hour + ':' + minute + ':' + second}</div> <hr /> <form> <div className='formElement'> <label>アラーム:<input type='time' onChange={this.changeAlarmTime} value={this.state.alermTime} /></label> </div> <div className='formElement'> {controllButton()} </div> </form> </div> ); } } |
Alarmコンポーネントでは以下の操作に対してアクション用の関数を定義しています。
- アラームにする時刻を設定:
changeAlarmTime
- アラームを設定:
setAlarm
- アラーム設定を解除:
clearAlarm
- アラーム状態を解除:
stopAlarm
changeAlarmTime
ではフォームの入力値をstateに更新しています。
setAlarm
ではenableAlarm
(アラーム状態の有効フラグ)をONにして、1秒おきに現在時刻がアラーム時刻になっているかのチェックをsetInterval
で行うようにします。
setInterval
内の処理でアラーム時刻になった場合、背景を赤にして音声を流すようになっています。
clearAlarm
ではsetAlarm
で設定したタイマーをリセットしており、
stopAlarm
ではアラームが流れた時に音声を停止した後にclearAlarm
を実行しています。
このような感じで、フォーム1つとボタン1つでタイマーの制御を行いました。
学んだこと
stateは極力、第一階層までのオブジェクトを格納すること
setState
でstateを更新する時、ネスト構造のオブジェクトを更新が簡単ではなかったため
stateはネストが深くならないように設計したいものです。
親から引き継いだpropsをstateに入れてはならない
Clockコンポーネントの時刻のstateをHourHandコンポーネントに渡してスタイルをpropsで引き継いだ値をstateに入れて更新しようとしたら、無限ループに陥ってしまいました。
propsの値はstateにしてはいけない。
UIの部品ごとにコンポーネントは分けた方が後から改修がしやすい←当たり前か
最初は前要素を一つのコンポーネントにして動かしていましたが、後からスタイルを微修正する時に
ソースコードでそれを修正する箇所が分かりづらかったので、UIのパーツごとにクラスを作り
それを組み合わせるようにすれば改修がしやすいです。
以上、React.jsを使えばタイマーアプリもオブジェクト指向で後から修正がしやすいように実装ができるという会でした。