React
サンプル1
React
の色々な機能などを使ってみようかと思い、サンプルを作ってみることにした。
基本的にできたものはCodePen
に置いておこう。
今回は基本的事項の確認として、
state
やprops
でのコンポーネント間の変数の受け渡しstyled-components
を利用したCSSの埋め込み- ボタン押下の処理
- コンポーネントのArrayの生成
これらの例を実装してみた。まずは、動作例を載せた後にちょっとした説明を書いていく。react
の説明の後はほぼ単なる実装メモになる。
動作例
これらの要素を使って連打力測定器のようなものを作った。測定方法は改善の余地がだいぶあるが、サンプルとしてはいいだろう。
See the Pen PushPush by ikapper (@ikapper) on CodePen.
stateやpropsでのコンポーネント間の変数の受け渡し(親→子)
コンポーネントはReact
でのGUI部品という感じで、htmlの要素を効率的にレンダリングしてくれるもの。
{}
で挟むことで変数に格納済みの子のコンポーネントを内部に含むこともできる。親子関係にあるコンポーネント間で親から子へ変数を受け渡す場合について考える。
子→親への変数の受け渡しはあまり行わない気がするのでとりあえず割愛。どちらかというと、子→親はイベントの伝播の方が多いと思う。
親→子への変数は、props
を使って渡すのがいい。親のApp
から子のBars
へと変数を渡す例:
class App extends React.Component {
...
render() {
// styled-componentsでスタイリング(後述)
const App = window.styled.div`...`;
const SiiyaButton = window.styled.button`...`;
return (
<App>
<Bars
countPerSec={this.state.countPerSec}
numOfBars={NUM_OF_BARS}
ceil={this.state.ceil}
/>
<SiiyaButton onClick={() => this.pound()}>連打しや</SiiyaButton>
</App>
);
}
...
}
この例では、3つの変数をBars
へと渡している。htmlの属性のように指定できる。また、親で管理する変数は、描画に使うものであれば、state
で管理するのがいいだろう。this.setState({"key": value})
で効率的レンダリングが行えるため。内部的に使うものならstate
にはしなくていいと思う。しても問題はないが。今回では、連打回数の算出に使うlastTime
をstate
で管理していない。
子からのイベントの伝播は、上記のonClick={() => this.pound()}
のように指定すれば十分だと思う。
ボタンの場合はonClick
で十分だが、そうでないときは子でthis.props.<props名>()
で実行すれば親のメソッドが実行される。
孫からの伝播でも子で同じことをすれば大丈夫:
class App extends React.Component {
...
render() {
return <Child evHandler={()=> this.somefunc()} />
}
somefunc() {
alert('ハロー')
}
}
class Child extends React.Component {
...
render() {
return <Grandchild grandEvHandler={() => this.props.evHandler()} />
}
}
class Grandchild extends React.Component {
...
render () {
return <button onClick={() => this.props.grandEvHandler()}>おして!</button>
}
}
これでApp
のsomefunc()
が呼ばれることだろう。props
を使うということを忘れずにおく。
この実装サンプルも作っておきたい。
コンポーネント間の変数・イベントの受け渡しについてはこれくらいにしておく。
styled-componentsを利用したCSSの埋め込み
styled-components
はCSSをReact
コンポーネントの定義内に書くことができるモジュール。CodePen
ではCDNで使える。
公式ドキュメントstyled-components: Basicsにある通り:
This style of usage requires the react CDN bundles and the react-is CDN bundle to be on the page as well (before the styled-components script.)
react-is
も同じページ内でロードしないといけない点に注意する必要がある。
また、window.styled
からアクセスできる点も注意がいる。
使用可能にすれば、下のようにすることで簡単にCSSとコンポーネントをまとめることができる:
class App extends React.Component {
...
render() {
const App = window.styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
`;
const SiiyaButton = window.styled.button`
flex: 1 1 auto;
width: 20em;
background-color: #eefeba;
font-family: "M PLUS Rounded 1c";
margin-top: 3px;
transition: all 1s ease;
&:hover {
background-color: #efabaa;
}
`;
return (
<App>
// ...
<SiiyaButton onClick={() => this.pound()}>連打しや</SiiyaButton>
</App>
);
}
}
おそらく、CSSを書くのはrender()
の外の方がいいことだけは注意。公式ドキュメントでは、スクリプトのグローバルスコープに定義してたように認識している。サンプルということで一箇所にまとめた結果、render()
内に定義となった。
細かい書き方は、先の公式ドキュメントを見るのが早い。擬似クラスの使用も楽ちん。
ボタン押下の処理
もう2回ほどサンプルを載せているが一応説明を。ボタンの場合は、html直書きと同じような感じで、onClick
にメソッドを渡せばいい。(onclick
ではない。)
this
の問題などは注意。無名関数で渡せば大体は大丈夫なイメージ:
...
render() {
...
return (
<App>
// ...
<SiiyaButton onClick={() => this.pound()}>連打しや</SiiyaButton>
</App>
);
}
...
このようにすれば、SiiyaButton
がクリックされた時に、App
のpound()
メソッドが呼ばれる。そのメソッド内で処理を行った後、this.setState()
を行えば状態を更新できる。
pound()
メソッドの中身は最後のコード全体の該当箇所にて。
コンポーネントのArray
React
ではコンポーネントをArrayでも保持・描画できる。<li></li>
などの生成が考えられるが、今回は<div></div>
のArrayを生成した。
今回はこのコンポーネントのArrayをメーターの目盛りとして使用する。
li
というかArrayでコンポーネントを扱うときは、props
にkey
を与えた方がいい。与えないとワーニングが出ると思う。key
はli
(リスト中のアイテム)が一意に決定できる文字列なら何でもいい。
今回の場合のArrayの実装:
class Bars extends React.Component {
render() {
const bars = [];
const delta = Math.floor(255 / this.props.numOfBars);
const progress = Math.floor(
(this.props.countPerSec / MAX_RENDA) * NUM_OF_BARS
);
// BarのArrayを生成
for (let i = 0; i < this.props.numOfBars; i++) {
const rr = i * delta;
const gg = 20;
const bb = 255 - rr;
const alpha = i < progress ? FRONT_ALPHA : BACK_ALPHA;
// #rrggbbの形式をキーとしておく
const code = `#${hexColor(rr)}${hexColor(gg)}${hexColor(bb)}`;
bars.push(<Bar key={code} rr={rr} gg={gg} bb={bb} alpha={alpha} />);
}
// スタイルは省略
const Bars = window.styled.div`...`;
const Count = window.styled.div`...`;
return (
<Bars>
<Count>
{this.props.countPerSec.toFixed(2)}回/s
<br />
MAX:{this.props.ceil.toFixed(2)}回/s
</Count>
{bars}
</Bars>
);
}
}
rr
とbb
をグラデーションにした。rr
とbb
の合計値を255にしている。rr=0
とbb=255
の組み合わせにならないことが起こる問題は許容した。そこまで正確でなくてもいいものだし。修正は合計の差分を均等に割り振る方法などがある。
カラーコードをkey
としておけばArrayの中では一意性を保てる。
Count
は重ねて表示して、Number.prototype.toFixed()
を使って小数2桁まで表示する。
Reactはこの上まで、あとは実装のメモ
大体のReact
の使用事項は済んだので、ここからは動作例の連打力測定の実装のメモをしていく。
Javascript
のコード全体はこちらにも載せておくが、実際のコード全体(html
, css
, javascript
)は動作例のCodePen
からも見れる。
Appの構造
まずはAppの全体構造から。シンプルにApp
を最上位として、メーター部分(Bars
)とボタン部分(SiiyaButton
)に分けている。
そして、Bars
はさらに子としてメーター1本ずつのBar
のArrayと、現在カウントと最高カウントを表示するCount
を持つ。
つまり構造は、
App
├── Bars
│ ├── Array of Bar
│ └── Count
└── SiiyaButton
このような感じになる。
CSSの観点からは、App
はflex
レイアウトで子を持つ。Bars
はBar
をflex
レイアウトで持つ。Count
はposition: absolute;
で重ねて表示する。
Appのすること
Appはコントローラ的な役割を果たす。なのでするべきことは他より少し多い。
- ボタンクリックのイベントハンドリング
- 1秒ごとの連打回数の管理
- 1秒ごとの最大連打回数の管理
管理はstate
を利用する。他のコンポーネントでは状態管理しないのでstate
は使用しない。
Barsのすること
Bars
はprops
として、1秒ごとの連打回数・最大連打回数、バーの本数を受け取る。
そして、連打回数と表示上の限界連打回数(MAX_RENDA
)から、現在の連打回数のMAX_RENDA
に対する達成度合いを算出する。
未到達のバーは透明度を下げて視覚的に限界と、現状の値をわかるようにしている。
一方で、バーの本数からバーの色のグラデーションの間隔を決定する。今回は255
を等分して変化をつけることにした。
アニメーションについて
フェードのアニメーションを使っている。これは、styled-components
のkeyframes
を使う。
window.styled.keyframes
で利用できる。
使い方は簡単で:
// keyframesを定義して
changeAlpha = window.styled.keyframes`
from {
background-color: rgba(${this.props.rr}, ${this.props.gg}, ${this.props.bb}, ${this.props.alpha});
}
to {
background-color: rgba(${this.props.rr}, ${this.props.gg}, ${this.props.bb}, ${BACK_ALPHA});
}
`;
// 要素に埋め込む
const Bar = window.styled.div`
flex: 1 1 auto;
background-color: rgba(${this.props.rr}, ${this.props.gg}, ${this.props.bb}, ${BACK_ALPHA});
animation: ${changeAlpha} 1s linear;
`;
のようにkeyframes
を定義して、styled-components
の定義内で埋め込めばいい。
カラーコード文字列の生成
rr
などの単体で渡して#rrggbb
の1色分の16進数を作成するヘルパー関数。
MDNにあったと思うが、URLのメモを忘れていた…。
少しコードを追加して、1桁の場合は0
埋めして2桁になるようにした。
function hexColor(c) {
if (c < 256) {
if (c < 16) {
return "0" + Math.abs(c).toString(16);
}
return Math.abs(c).toString(16);
}
return 0;
}
// 16進数のカラーコードが生成できる
const code = `#${hexColor(rr)}${hexColor(gg)}${hexColor(bb)}`;
連打回数の算出方法
自然な感じに見せるのは結構難しい。一定時間連打し続けて、その後に連打した回数を一定時間で割れば1秒あたりの回数は出るが、何となくそういう方法にはしたくなかった。
結局は、1回のクリックから次のクリックまでかかった時間の逆数を1秒あたりの連打回数にすることにした。この方法の欠点は、描画間隔がクリックの仕方次第になってしまうこと。連打すればするほど描画回数が多くなり、処理落ちなどが発生すると連打回数が正しく計算できず、あっという間に30回/秒をオーバーしてしまうこともあるようだ。(下図参照) さらに、クリックをやめても直近の回数は残ってしまう。これの対処は簡単そうだが。
壊れた例:
1秒に42回もクリックできるわけないだろ😅 気持ちいい算出方法は、上記2つのハイブリッド的な感じだろうか。最初は後者で測定して、安定してきたらn秒ごとの回数をとる、というようなイメージ。
遭遇したちょっとしたスタイリングの問題
今回styled-components
は使う予定がなかった。
しかし、スタイリングをrender()
の中で変数を作って埋め込む形で行う、つまり、下のようなコードでエラーが起きるようなのでstyled-components
を使用することにした。
render() {
// なぜかエラーに。reactのminifyの問題?
const _style = `background-color: ${this.props.colorCode};`
return (
<div style={_style}>{this.props.colorCode}</div>
)
}
コメントアウトにもあるように、おそらくminify
の問題だと思う。変数が展開されるためか?と思われる。
こういうわけでstyled-components
を使うことになったが、結局はコードのまとまりがよくなった気がする。個人的には見やすい。
クラス名などを考える必要がないし、CSSのファイルを見る必要も関連付けを考える必要もないのは楽だ。
javascriptコード全体
最後に、縦長になってしまうが、実装したjavascript
というかjsx
全体のコードを載せておく。
const NUM_OF_BARS = 10;
const MAX_RENDA = 18;
const BACK_ALPHA = 0.2;
const FRONT_ALPHA = 0.8;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
countPerSec: 0,
ceil: 0
};
}
render() {
const App = window.styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
`;
const SiiyaButton = window.styled.button`
flex: 1 1 auto;
width: 20em;
background-color: #eefeba;
font-family: "M PLUS Rounded 1c";
margin-top: 3px;
transition: all 1s ease;
&:hover {
background-color: #efabaa;
}
`;
return (
<App>
<Bars
countPerSec={this.state.countPerSec}
numOfBars={NUM_OF_BARS}
ceil={this.state.ceil}
/>
<SiiyaButton onClick={() => this.pound()}>連打しや</SiiyaButton>
</App>
);
}
pound() {
const currentTime = performance.now();
if (this.lastTime == null) {
this.lastTime = currentTime;
} else {
const delta = currentTime - this.lastTime;
this.lastTime = currentTime;
// calc hz
const cPerSec = (1 / delta) * 1000;
if (cPerSec > this.state.ceil) {
this.setState({
countPerSec: cPerSec,
ceil: cPerSec
});
} else {
this.setState({
countPerSec: cPerSec
});
}
}
}
}
class Bars extends React.Component {
render() {
const bars = [];
const delta = Math.floor(255 / this.props.numOfBars);
const progress = Math.floor(
(this.props.countPerSec / MAX_RENDA) * NUM_OF_BARS
);
for (let i = 0; i < this.props.numOfBars; i++) {
const rr = i * delta;
const gg = 20;
const bb = 255 - rr;
const alpha = i < progress ? FRONT_ALPHA : BACK_ALPHA;
const code = `#${hexColor(rr)}${hexColor(gg)}${hexColor(bb)}`;
bars.push(<Bar key={code} rr={rr} gg={gg} bb={bb} alpha={alpha} />);
}
const Bars = window.styled.div`
display: flex;
flex-direction: column-reverse;
flex: 3 1 auto;
width: 20em;
`;
const Count = window.styled.div`
position: absolute;
top: 14px;
left: 0;
right: 0;
text-align: center;
font-size: 2em;
font-family: "Nico Moji", "M PLUS Rounded 1c";
`;
return (
<Bars>
<Count>
{this.props.countPerSec.toFixed(2)}回/s
<br />
MAX:{this.props.ceil.toFixed(2)}回/s
</Count>
{bars}
</Bars>
);
}
}
class Bar extends React.Component {
render() {
let changeAlpha = null;
if (this.props.alpha >= 0.5) {
changeAlpha = window.styled.keyframes`
from {
background-color: rgba(${this.props.rr}, ${this.props.gg}, ${this.props.bb}, ${this.props.alpha});
}
to {
background-color: rgba(${this.props.rr}, ${this.props.gg}, ${this.props.bb}, ${BACK_ALPHA});
}
`;
}
const Bar = window.styled.div`
flex: 1 1 auto;
background-color: rgba(${this.props.rr}, ${this.props.gg}, ${this.props.bb}, ${BACK_ALPHA});
animation: ${changeAlpha} 1s linear;
`;
return <Bar></Bar>;
}
}
function hexColor(c) {
if (c < 256) {
if (c < 16) {
return "0" + Math.abs(c).toString(16);
}
return Math.abs(c).toString(16);
}
return 0;
}
ReactDOM.render(<App />, document.getElementById("app"));
おわり
だいぶ長くなってしまった。サンプルとしては動作例のCodePen
を見れば十分だと思う。
React
の基本の一部なので使いこなしておきたい。
以上です。
広告