20210606のReactに関する記事は13件です。

Reactでコンテキストメニューを作るライブラリの比較

目的 Reactでコンテキストメニューを作るライブラリが数種類あるので、比較の覚書です 環境 React 17.0.1 react-dom 17.0.1 比較 react-contextmenu https://github.com/vkbansal/react-contextmenu 動作サンプル https://vkbansal.github.io/react-contextmenu/#/ 良い点 サポートされているブラウザが多い 複雑なメニューも実装できる Right to Leftに対応している ダウンロードが随時50,000前後と多く、よく選ばれている 課題 2020/8にサポートを停止したため、自分でforkしてメンテナンスする必要がある https://github.com/vkbansal/react-contextmenu/issues/339 スタイル管理機能はない 標準でCSSは用意されていないため自分で指定する必要がある サンプル は提供されています styled-componentsなどStyles in JSなフレームワークと組み合わせにくいかもしれない react-contextify https://github.com/fkhadra/react-contexify 動作サンプル https://fkhadra.github.io/react-contexify/ 良い点 ある程度サポートやライブラリの更新が行われている TypeScript対応している サブメニューが簡単に作れる ドキュメントが充実している スタイル管理機能 がある Sass, styled-component を利用できる テーマを利用できる 課題 サポートやメンテナンスが少し停滞気味 2021/06の最新は 2020/12リリースのv5.0.0 で半年更新がない Material UI https://material-ui.com/ 動作サンプル https://material-ui.com/components/menus/ 良い点 サポートや機能開発が定期的に行われる 細かくスタイルを設定できる TypeScript対応している 課題 将来的にはMaterial UIのv5とv4どちらを採用するか、周辺ライブラリが対応しているMaterial UIのバージョンと合わせて判断が必要 2021/06/06時点でMaterial UI 5はアルファ版 UIしか提供されないので基本は全て実装が必要 サブメニューや、コンテキストメニューでよく使われるロジック、Hookなどは標準で用意されていない material-ui-nested-menu-item などMaterial UIベースのライブラリがあるが、更新頻度が低かったり要件に合わなかったりする まとめ Reactでコンテキストメニューを作るライブラリは サポートや継続的な機能開発がないライブラリもある スタイル管理、TypeScriptサポートの有無はライブラリによる 作者は react-contextify を採用しましたが、更新頻度が落ちているので今後のメンテナンスは要検討だと思いました 他にも良いライブラリがあったら是非教えてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる⑦_ブロックの種類を増やす

そろそろ、テトラミノの数を増やしたい。 わりとあっさり増やせた。 reset(n):初期化の部分でブロックの形をランダムに指定する方式とした var minoT = (()=>{ var init=[[ [0,0,0], [1,1,1], [0,1,0], ],[ [1,1,0], [1,1,0], [0,0,0], ],[ [1,1,0], [0,1,1], [0,0,0], ],[ [0,1,1], [1,1,0], [0,0,0], ],[ [1,0,0], [1,1,1], [0,0,0], ],[ [0,0,1], [1,1,1], [0,0,0], ],[ [1,0,1], [1,1,1], [0,0,0], ],] var T=[] var rotate=function(){ let newT=JSON.parse(JSON.stringify((new Array(3)).fill((new Array(3)).fill(0)))) T.map((d,i)=>{ T[i].map((e,j)=>{ newT[i][j] = T[2-j][i] }) }) T=newT } return { rotate:function(){ rotate() return this }, reset:function(n=0){ T=init[n] return this }, val:function(x,y){ let newT=T for(let i = 0;i<y;i++){ newT=[[0,0,0]].concat(newT) } for(let j = 0;j<x;j++){ newT=newT.map(d=>[0].concat(d)) } return newT } } })()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる⑥_レンダリングの負荷を抑える

テトリスなどのDOMの動きが激しいものは、レンダリングの負荷を抑えた方がよい。 今までは、画面全部を毎回描画していたが、更新部分だけの描画にして、負荷を抑えたい まずは、描画を変動部分と静的部分に分けた テトリスの枠とか、すでに底に落ちたブロックは静的部分として、落ちてくるブロックを動的部分とした。 毎回の描画は、落ちてくるブロックを描画する。 ブロックが底まで落ちたら、そのブロックを静的部分に追加する。 return ( <div onKeyDown={(e) => {keydown(e)}} tabIndex="0"> <Flow disp={disp}/> <Static base={base}/> </div> ) これだけだと、setStateの呼び出しでStaticもレンダリングされてしまうので、memo化などをすることになる memo化 const Static = React.memo(({...props})=> { let d = [] for(let i = 0; i < props.base.length; i++) { for(let j = 0; j < props.base[i].length; j++) { (props.base[i][j]===1) ? d.push(<Dot x={j*20} y={i*20}/>) : null } } return ( <> {d} </> ) })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる⑤_キーボード入力イベントをつける

以前書いたもの var minoT = (()=>{ var init=[ [0,0,0], [1,1,1], [0,1,0], ] var T=[] var rotate=function(){ let newT=JSON.parse(JSON.stringify((new Array(3)).fill((new Array(3)).fill(0)))) T.map((d,i)=>{ T[i].map((e,j)=>{ newT[i][j] = T[2-j][i] }) }) T=newT } return { val: function(x,y){ let newT=T for(let i = 0;i<y;i++){ newT=[[0,0,0]].concat(newT) } for(let j = 0;j<x;j++){ newT=newT.map(d=>[0].concat(d)) } return newT }, rotate:function(){ rotate() return this }, reset:function(){ T=init return this }, } })() 今回書いたもの const keydown = (e) => { switch(e.which){ //Enter case 13: minoT.rotate() setDisp(minoT.val2(X, Y)) break; //Right case 39: if(checkCrush(base, minoT.val2(X+1, Y))) { break } setX(X+1) setDisp(minoT.val2(X+1, Y)) break; //Left case 37: if(checkCrush(base, minoT.val2(X-1, Y))) { break } setX(X-1) setDisp(minoT.val2(X-1, Y)) break //Down case 40: if(checkCrush(base, minoT.val2(X-1, Y))) { break } setY(Y+1) break default: break; } 右、左、下の動きを付けた。 一定時間ごとに描画を更新する処理は、こつがいる。 useEffectでやるにしても、タイミングをしっかりつかまないとレンダー過剰になりがちである。 色々考えた結果、周期的レンダー用に関数をつくった。 以下のロジックは、ブロックを下に1マス進めつつ、次のレンダーを1秒後に指定している。 これが、setTimeoutにブロックを進める処理を入れるとうまくいかない。 なぜならば、最後に一秒後に1マス多く進むバグがでやすい。timeoutをキャンセルなどをやりはじめると複雑になりがち const [effect, setEffect]=useState(false) useEffect(()=>{ setDisp(minoT.val2(X, Y)) setY(Y+1) timeout = setTimeout(()=>{ setEffect(!effect) },1000) },[effect])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる④_落下で衝突判定

export const Tetris = () => { const [disp, setDisp] = useState(Base) const [X, setX] = useState(4) const [Y, setY] = useState(0) const [base, setBase] = useState(disp) useEffect(()=>{ minoT.reset() },[]) useEffect(()=>{ setDisp(merge(base, minoT.val2(X, Y))) let timeout = setTimeout(()=>{ console.log("aa") setY(Y+1) },1000) if(checkCrush(base, minoT.val2(X, Y))) { clearTimeout(timeout) setBase(merge(base, minoT.val2(X, Y))) minoT.reset() setY(0) } },[X,Y]) return ( <> <Display disp={disp}/> </> ) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる③_枠とミノを統合する

とりあえず、こんなかんじにしてみる。 落下の細かい処理はまた後でやることにする 前回までに書いた部分 const Base=[ [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1,1], ] function Display(props) { let d = [] for(let i = 0; i < props.disp.length; i++) { for(let j = 0; j < props.disp[i].length; j++) { (props.disp[i][j]===1) ? d.push(<Dot x={j*20} y={i*20}/>) : null } } return ( <> {d} </> ) } function Dot(props){ return <div style={{'width': '20px', 'height': '20px', 'backgroundColor':'red','position':'absolute', 'top':props.y,'left':props.x}}></div> } var minoT = (()=>{ var init=[ [0,0,0], [1,1,1], [0,1,0], ] ...以下省略 今回追加した部分 export TetrisTetris = () => { const [disp, setDisp] = useState(Base) const [mino, setMino] = useState(minoT.reset().val2(4,0)) const [dispD, setDispD] = useState(Base) useEffect(()=>{ setTimeout(()=>{ console.log("a") setMino([[0,0,0,0,0]].concat(mino)) },1000) setDisp(merge(dispD, mino)) },[mino]) return ( <> <Display disp={disp}/> </> ) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

useContext をラップする

要約 - 呼び出し側は useContext(CounterContext) ではなく useCounter() を使う - useCounter の引数に呼び出し側の自由度を持てる // CounterContext.tsx import { createContext, ReactNode, useContext, useState } from "react"; type Value = { count: number; increment: () => void; decrement: () => void; }; const CounterContext = createContext<Value | undefined>(undefined); type Props = { children: ReactNode }; const CounterProvider = ({ children }: Props) => { const [count, setCount] = useState(0); const increment = () => setCount((prev) => prev + 1); const decrement = () => setCount((prev) => prev - 1); const value: Value = { count, increment, decrement, }; return ( <CounterContext.Provider value={value}>{children}</CounterContext.Provider> ); }; const useCounter = () => { const value = useContext(CounterContext); if (!value) throw new Error("useCounter must be used within a CounterProvider"); return value; }; export { CounterProvider, useCounter };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる②_テトロミノをつくる

テトロミノの「T」型をつくってみる 現在のミノの形を保持するためにクロージャ型にしてみる とりあえずこんな形をベースとした var minoT = (()=>{ var init=[ [0,0,0], [1,1,1], [0,1,0], ] var T=[] return { val: function(){ return T } reset:function(){ T=init return this } } }) 回転させる関数が必要だ var rotate=function(){ let newT=JSON.parse(JSON.stringify((new Array(3)).fill((new Array(3)).fill(0)))) T.map((d,i)=>{ T[i].map((e,j)=>{ newT[i][j] = T[2-j][i] }) }) T=newT } 落下位置を表現しなければ 落下位置は引数で指定して、出来上がった配列を受け取る仕組みとする val: function(x = 0,y = 0){ let newT=T for(let i = 0;i<y;i++){ newT=[[0,0,0]].concat(newT) } for(let j = 0;j<x;j++){ newT=newT.map(d=>[0].concat(d)) } return newT }, これでどうだろう? /*完成*/ var minoT = (()=>{ var init=[ [0,0,0], [1,1,1], [0,1,0], ] var T=[] var rotate=function(){ let newT=JSON.parse(JSON.stringify((new Array(3)).fill((new Array(3)).fill(0)))) T.map((d,i)=>{ T[i].map((e,j)=>{ newT[i][j] = T[2-j][i] }) }) T = newT } return { val: function(x = 0, y = 0){ let newT=T for(let i = 0;i<y;i++){ newT=[[0,0,0]].concat(newT) } for(let j = 0;j<x;j++){ newT=newT.map(d=>[0].concat(d)) } return newT }, rotate:function(){ rotate() return this }, reset:function(){ T=init return this } } })() コンソールに出力してみる console.log(minoT.reset().val()) /* [ [ 0, 0, 0 ], [ 1, 1, 1 ], [ 0, 1, 0 ], ] */ console.log(minoT.rotate().val(3,1)) /* [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 1, 0 ], ] */ きちんと動きできてる これを前回つくった描画機能に入れれば描画される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Context Provider をまとめる

redux の代替として context api と hooks で状態管理をするときに、Provider が何重にもなるときありませんか? <AuthProvider> <UsersProvider> <TasksProvider> {children} <TasksProvider /> <UsersProvider /> <AuthProvider /> 「気にしない」または「一つの Provider に全部詰め込む」 でもいいですが、ComposeProviders みたいなものを用意するのもありです。 // ComposeProviders.tsx import { JSXElementConstructor, PropsWithChildren, ReactNode } from "react"; type Props = { providers: JSXElementConstructor<PropsWithChildren<any>>[]; children: ReactNode; }; export default function ComposeProviders(props: Props) { const { providers = [], children } = props; return ( <> {providers.reduceRight( (acc, Provider) => ( <Provider>{acc}</Provider> ), children )} </> ); } 先に示した、例が以下のようにスッキリ書けます。 <ComposeProviders providers={[AuthProvider, UsersProvider, TasksProvider]}> {children} <ComposeProviders />
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React チュートリアル発展 追加課題

React公式チュートリアル追加課題の実装に関してまとめます。前回の記事で関数コンポーネント化を行った後の続きです。まだの方は先に確認して頂ければと思います。 https://qiita.com/nishiwaki_ff/items/d60f2ba346521610775b 追加実装の流れ 公式チュートリアルのタイムトラベル機能実装まで完了(https://ja.reactjs.org/tutorial/tutorial.html#wrapping-up) モジュール化 関数コンポーネントに書き換え 追加課題の実装 ⇦この記事 追加課題1 解説 課題1の内容は、履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示するです。この課題を実装するには次の2つのことを実現する必要があります。 着手の位置をstateで保持する (col, row) のフォーマットで表示する 着手の位置や、それを表示する履歴情報を持っているのはGameコンポーネントです。では順番にGameコンポーネントに修正を加えます。 着手の位置をstateで保持する ボードが要素9つの配列になってるので、そのインデックスを着手の位置として保持しようと思います。最終的に(col, row) のフォーマットで表示する必要があるものの、インデックスから計算で導き出すことができるので、インデックスのみをstateに保持させます。 Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null // 着手の位置を保持する為pointを追加 } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i // pointにインデックスを代入 } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move + '(' + step.point + ')' : // 確認の為着手位置を表示 'Go to game start'; // 以下変更無し ここまで修正を終えるとこのような結果になると思います。あとは表示を修正すれば完成です。 (col, row) のフォーマットで表示する Game.js const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; // colを算出 const row = (step.point / 3 + 1) | 0; // rowを算出 const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; // 指定のフォーマットに修正 const desc = move ? goToMove : 'Go to game start'; // 1行に修正 return ( <li key={move}> <button onClick={() => jumpTo(move)}>{desc}</button> </li> ); }); // 以下変更無し colとrowを算出した後、(col, row) のフォーマットで表示させています。rowに関してはビット論理和を用いて小数点以下を切り捨てています。ビット論理和の代わりにMath.floor関数を使うことも可能です。 以上で課題1は終了です。このような結果になります。 追加課題2 解説 課題2の内容は、着手履歴のリスト中で現在選択されているアイテムをボールドにするです。この課題を実装するには次の2つのことを実現する必要があります。 履歴の表示をボールドにする 現在選択されているアイテムのみをボールドにする 履歴の表示をボールドにする まずは着手履歴リストのbuttonタグにclassを追加します。 Game.js const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={'bold'} // class追加 > {desc} </button> </li> ); }); // 以下変更無し 追加したclassに対応する記述をcssに追加します。 index.css /* 追加 */ .bold { font-weight: bold; } 履歴の表示が全てボールドになります。 現在選択されているアイテムのみをボールドにする 最後に現在選択されているアイテムのみボールドに修正していきます。現在選択されているアイテムを判定するにはstemNumberが使えます。 Game.js const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} // 現在選択されているアイテムだけclassNameをboldにする className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); // 以下変更無し 以上で課題2が終了です。 追加課題3 解説 課題3の内容は、Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換えるです。この課題は繰り返し文を利用して実装します。まずは一部だけfor文で書き換えます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} /> ); }; const maxCol = 3; const rowBoard1 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 0 + col; rowBoard1.push(renderSquare(index)); } return ( <div> <div className="board-row"> {rowBoard1} </div> <div className="board-row"> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className="board-row"> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </div> ); }; export default Board; rowBoard1の箇所を追加及び修正しました。しかしこの時点で警告が出ます。 keyが必要なので追加します。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} // key追加 /> ); }; // 以下変更無し これで警告も消えたので先程と同様にfor文で書き換えていきます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} /> ); }; const maxCol = 3; const rowBoard1 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 0 + col; rowBoard1.push(renderSquare(index)); } const rowBoard2 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 1 + col; rowBoard2.push(renderSquare(index)); } const rowBoard3 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 2 + col; rowBoard3.push(renderSquare(index)); } return ( <div> <div className="board-row"> {rowBoard1} </div> <div className="board-row"> {rowBoard2} </div> <div className="board-row"> {rowBoard3} </div> </div> ); }; export default Board; 明らかに冗長なのでさらにfor文を追加してネストさせます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; rowBoard.push(renderSquare(index)); } squareBoard.push( <div className="board-row"> {rowBoard} </div> ); } return ( <div> {squareBoard} </div> ); }; export default Board; これでほぼ完成ですが、ここで再び警告が出ます。 先程と同様にkeyを追加すると警告が消えます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; rowBoard.push(renderSquare(index)); } squareBoard.push( <div className="board-row" key={'row-' + row} // key追加 > {rowBoard} </div> ); } return ( <div> {squareBoard} </div> ); }; export default Board; 以上で課題3が終了です。 追加課題4 解説 課題4の内容は、着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加するです。次の順番で実装していきます。 現在は昇順で表示されているので、降順で表示させる 昇順と降順を判定するためのstateを追加 ボタンを追加し、ボタン押下で並べ替えができるようにする 降順で表示させる 降順で表示させるにはreverse()メソッドを用いてmovesの順序を入れ替えることで実現できます。 Game.js const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); moves.reverse(); // movesの順序入れ替え // 以下変更無し 昇順と降順を判定するためのstateを追加 Game.js const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); // state追加 const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); // stateの値で昇順と降順を判定し条件分岐 if (movesOrder) { moves.reverse(); } // 以下変更無し ボタンを追加し、ボタン押下で並べ替えができるようにする まずはボタンを追加します。 Game.js return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> {/* ボタン追加 */} <button> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); // 以下変更無し ボタンを追加しましたが、まだ押下しても何も変化しません。最後にボタンを押下した時にstateの値が変わるようイベントを追加します。 Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> {/* イベント追加 */} <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); }; const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }; export default Game; 以上で課題4が終了です。 追加課題5 解説 課題5の内容は、どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトするです。次の順番で実装していきます。 calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする 3つのマスの情報を用いて該当箇所をハイライトする calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする calculateWinner関数の中身とそれを呼び出していた箇所を修正します。 Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares).winner || squares[i]) { // winnerを参照するよう修正 return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares).winner; // winnerを参照するよう修正 const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); }; const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; // 勝者とマスの情報をもつオブジェクトを用意 const result = { winner: null, winLine: [] } for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { result.winner = squares[a]; // 勝者をresultに代入 result.winLine = result.winLine.concat(lines[i]); // 勝利に繋がったマスの情報をresultに代入 } } return result; // resultを返す }; export default Game; 3つのマスの情報を用いて該当箇所をハイライトする calculateWinner関数から返るマスの情報をまずはBoardコンポーネントに渡します。 Game.js const current = history[stepNumber]; const {winner, winLine} = calculateWinner(current.squares); // winnerとwinLineを分割代入 const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> {/* propsにwinLineを追加 */} <Board squares={current.squares} onClick={i => handleClick(i)} winLine={winLine} /> </div> <div className="game-info"> <div>{status}</div> <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); // 以下変更無し これでBoardコンポーネントでwinLineが参照できるようになります。次にBoardコンポーネントを修正します。propsで受け取ったwinLineにindexが含まれるかどうか確認し、結果をSquareコンポーネントに渡します。 Board.js const Board = (props) => { const renderSquare = (i, isHighlight) => { // isHighlightでハイライト有無の情報を受け取る return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} // props追加 isHighlight={isHighlight} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; const isHighlight = props.winLine.includes(index); // winLineにindexが含まれるかどうか確認 rowBoard.push(renderSquare(index, isHighlight)); } squareBoard.push( <div className="board-row" key={'row-' + row} > {rowBoard} </div> ); } // 以下変更無し これでSquareコンポーネントでハイライト有無の情報が参照できるようになります。その情報を用いてハイライトするようSquareコンポーネントを修正します。 Square.js const Square = (props) => { const className = props.isHighlight ? 'square-highlight' : 'square'; return ( <button className={className} onClick={props.onClick}> {props.value} </button> ); }; export default Square; 最後に対応するcssを追加して完了です。 index.css /* 追加 */ .square-highlight { background: yellow; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } 追加課題6 解説 課題6の内容は、どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示するです。次の順番で実装していきます。 calculateWinner関数で引き分けかどうかの情報も返すようにする 引き分けの場合、引き分けのメッセージを表示する calculateWinner関数で引き分けかどうかの情報も返すようにする Game.js const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; const result = { winner: null, winLine: [], isDraw: false // 追加 } for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { result.winner = squares[a]; result.winLine = result.winLine.concat(lines[i]); } } // 勝者が決まっておらず、かつマスが全て埋まっている時が引き分けになる if (result.winner === null && !squares.includes(null)) { result.isDraw = true; } return result; }; export default Game; 引き分けの場合、引き分けのメッセージを表示する Game.js const current = history[stepNumber]; const {winner, winLine, isDraw} = calculateWinner(current.squares); // isDrawも参照できるように書き換え const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } // 引き分けだった場合はstatusを変更 if (isDraw) { status = 'Draw'; } // 以下変更無し これで引き分けの場合はDrawのメッセージが表示されるようになります。 まとめ 以上で全ての実装が終了です。 最後に最終結果のコードがこちらです。 index.js import ReactDOM from 'react-dom'; import './index.css'; import Game from './Game.js'; ReactDOM.render(<Game />, document.getElementById("root")); Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares).winner || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const {winner, winLine, isDraw} = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } if (isDraw) { status = 'Draw'; } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} winLine={winLine} /> </div> <div className="game-info"> <div>{status}</div> <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); }; const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; const result = { winner: null, winLine: [], isDraw: false } for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { result.winner = squares[a]; result.winLine = result.winLine.concat(lines[i]); } } if (result.winner === null && !squares.includes(null)) { result.isDraw = true; } return result; }; export default Game; Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i, isHighlight) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} isHighlight={isHighlight} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; const isHighlight = props.winLine.includes(index); rowBoard.push(renderSquare(index, isHighlight)); } squareBoard.push( <div className="board-row" key={'row-' + row} > {rowBoard} </div> ); } return ( <div> {squareBoard} </div> ); }; export default Board; Square.js const Square = (props) => { const className = props.isHighlight ? 'square-highlight' : 'square'; return ( <button className={className} onClick={props.onClick}> {props.value} </button> ); }; export default Square; index.css body { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; } ol, ul { padding-left: 30px; } .board-row:after { clear: both; content: ""; display: table; } .status { margin-bottom: 10px; } .square { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } .square__highlight { background: yellow; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } .square:focus { outline: none; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } .bold { font-weight: bold; } .square-highlight { background: yellow; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる①_枠をつくる

はじめに Qiita投稿のための取り組みとして、テトリスをつくりたいと思います。 なぜテトリスにしたかというと、有名なゲームであり難易度も多少高いと思うからです。 テトリスの作り方には様々なやり方があり、それらは既にノウハウとしてWebにアップされているものもあります。 それらと同じことをやっても、やる方も見るほうもあまり意味ありません。まだ、だれもやっていないやり方でやろうと思うわけです。 今回は上記の観点で、以下の方法でプログラムすることにします。 画面を方眼紙のように見立てて、配列で表示を全渡しする。 配列は1次元配列にする 画面をドットにして渡す const Base=[ [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1,1], ] function Display(props) { let d = [] for(let i = 0; i < props.disp.length; i++) { for(let j = 0; j < props.disp[i].length; j++) { (props.disp[i][j]===1) ? d.push(<Dot x={j*20} y={i*20}/>) : null } } return ( <> {d} </> ) } function Dot(props){ return <div style={{'width': '20px', 'height': '20px', 'backgroundColor':'red','position':'absolute', 'top':props.y,'left':props.x}}></div> }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Map Reactを簡単に試す

完成形 Google Map React でも紹介されているGoogle Map React。いくつか過去記事がある。 今回使うのは以下。 npm install --save google-map-react サンプル Main.js import React, { Component } from 'react'; import GoogleMapReact from 'google-map-react'; const AnyReactComponent = ({ text }) => <div>{text}</div>; class SimpleMap extends Component { static defaultProps = { center: { lat: 35.66, // 緯度経度 lng: 139.74 }, zoom: 15 }; render() { return ( // Important! Always set the container height explicitly <div style={{ height: '100vh', width: '100%' }}> <GoogleMapReact bootstrapURLKeys={{ key:'{API キーをここに}' }} defaultCenter={this.props.center} defaultZoom={this.props.zoom} > <AnyReactComponent lat={35.667345081692176} lng={139.7401442420512} text="アークヒルズはここ" /> </GoogleMapReact> </div> ); } } export default SimpleMap; トラブルシューティング 依存関係の解決 npm install --save --legacy-peer-deps google-map-react APIキー このページでは Google マップが正しく読み込まれませんでした。JavaScript コンソールで技術情報をご確認ください。 「Google Maps JavaScript API」で「有効にする」ことで、このエラーを抑止して、利用できるようになります。 You must enable Billing on the Google Cloud Project at https://console.cloud.google.com/project/_/billing/enable Learn more at https://developers.google.com/maps/gmp-get-started Zoomとは Zoom は 0 to 18。 ということで超簡単にできた!という以上メモ書きだが、なにがしか参考になればさいわいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub Actionsで React + Firebase のプロジェクトをCI/CDで自動デプロイした話

先日のWebサービスを作った際に2020年にリリースされたGitHubActionsでCI/CD (継続的インテグレーション/継続的デリバリー)をしてみたので、軽く流れを投稿します。 CI/CDすることになった経緯 プロジェクトを修正した後に毎回ビルドとかデプロイを実行するのが面倒なので、自動化したい トレンドのCI/CDを使ったことがないので勉強したい GitHub Actions設定の流れ CI/CDサーバーがFirebaseにアクセスするためのトークンの取得&環境変数に設定 firebase login:ciコマンドを実行する。 コンテナや WSL でコマンドを実行する場合は--no-localhostオプションを付けておく。 ブラウザが立ち上がるのでgoogleアカウント認証する。 トークンが表示されるので、コマンドラインにトークンを入力する。 コマンドラインにCI/CDサーバーに登録するトークンが表示される。 GitHubのSettingsタブ → Secrets からName=FIREBASE_TOKEN_PROD,value=コマンドラインに表示されたトークンを追加する。 CI/CDの設定ファイル作成 GitHubのActionsタブ → ファイル名を deploy.yml に変更する →Start Commitボタンを押す。 ワークツリー(GItの作業ディレクトリ)でGit pullし、作業環境にdeploy.ymlを取り込む。 ワークツリーで"./.github/workflows/deploy.yml"を開いて設定ファイルを修正する。 deploy.yml # This is a basic workflow to help you get started with Actions # これは、アクションを開始するのに役立つ基本的なワークフローです。 name: React Firebase CICD # Controls when the action will run. # アクションが実行されるタイミングを制御します。 on: # Triggers the workflow on push or pull request events but only for the master branch # プッシュまたはプル リクエスト イベントでワークフローをトリガーしますが、マスター ブランチのみを対象とします push: branches: [master] pull_request: branches: [master] # Allows you to run this workflow manually from the Actions tab # [アクション] タブからこのワークフローを手動で実行できます workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel # ワークフローの実行は、順次または並行して実行できる 1 つ以上のジョブで構成されます。 jobs: # This workflow contains a single job called "build" # このワークフローには、「ビルド」と呼ばれる単一のジョブが含まれています build: # The type of runner that the job will run on # ジョブが実行されるランナーのタイプ runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job # ステップは、ジョブの一部として実行される一連のタスクを表します steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it # $GITHUB_WORKSPACE の下にあるリポジトリをチェックアウトして、ジョブがアクセスできるようにします - name: Checkout uses: actions/checkout@v2 # Runs a single command using the runners shell # runners シェルを使用して単一のコマンドを実行します - name: Run a one-line script run: echo Start Deploy! # Nodeのインストール - name: Install Node uses: actions/setup-node@v1 with: node-version: 14.16.1 # runners 依存関係のインストール - name: Install Dependencies run: | npm install working-directory: ./ # ビルド実行 - name: Build run: | npm run build working-directory: ./ # テスト実行(テストコードが無い場合はエラーが発生するので、コメントアウトすること。) - name: Test run: | npm test working-directory: / # デプロイ準備 - name: Install Firebase Tools run: | npm install -g firebase-tools working-directory: ./ # デプロイ実行 # FIREBASE_TOKEN_PRODはGitの環境変数から呼び出される - name: Deploy env: FIREBASE_TOKEN_PROD: ${{ secrets.FIREBASE_TOKEN_PROD }} run: | firebase deploy --only hosting --token $FIREBASE_TOKEN_PROD working-directory: ./ CI/CD実行 ワークツリーでgit pushする。 GitHubのActionsタブを開くと設定したスクリプトが実行されているのが見えるので、コミット名をクリックする。 全てのタスクが成功すると、こんな表示になる。 感想など CI/CDって名前だけ聞くと難しそうに感じるが実際にやってみると、最低限の機能を動かすだけであれば、そんなに難しくなさそうなことが分かった。 設定ファイルはdocker-composeファイルに似ているので既視感を感じる。 試していないが、工夫するとデプロイ完了時とか失敗時にSlackとかで通知を送ることができそう。 自分の場合、テストコードを書いていなかったので、近日中に試してみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む