Reactのチュートリアルをやってみた
これまで仕事ではサーバーサイドが中心だったので、フロント技術を少し触れてみようかと。
今回は仕事のフロントサイドで使用されていたReactを選択。
公式サイト(↓)で三目並べを作るチュートリアルと練習問題が用意されていたので、とりあえずやってみた。
■チュートリアルを終えての感想
現状、思っていたよりは難しくない
仕事で障害対応の時に何となく見ていたので案外スッと理解できた。
「パーツを作って組み合わせて画面を作る」と聞いていて、なんだそれ状態だったが意味が分かった。
サーバーでプライベート関数作って処理を切り分けるのと同じイメージ。
他の箇所で再利用しやすくなるし、同じコードを何回も書く手間を減らせて便利だなと感じた。とは言え、当然難しい部分もある
コンポーネントの適切な切り分け。
⇒慣れもあるだろうが、現状ではどこで分離すればいいのか判断がつかない。
useStateの使いどころ。
⇒色々操作して再レンダリングが実行される中でも値を保持したい場合に使う?
HTMLの部分とJavaScriptの部分が混在するので読みにくい。
⇒完成画面の全体像が把握できてないと、どこがどこに当たるのか理解するのが大変。
チュートリアルですら難しいなーと感じたので、
未経験で入社して研修上がりでいきなりReactでの開発だったら挫折する人いそう。
型が無いのがしんどい。
⇒これはReactというよりJavaScriptの話なのでTypeScriptと組み合わせれば解決できそうだが。これから
まだ公式サイトに学習コンテンツがあるので一通り見てみようと思う。
その後はReactフロント、C#サーバーで何か簡単なWebアプリが作てみようかなぁ。
■作成したコード
自作したコード
- 一応練習問題含めて、動くものはできた
- コメントがスカスカだったり、二重の三項演算子がある等の問題はあるが自力で組めたので大甘判定で良しとする
import { useState } from "react"; function Square({ value, onSquareClick, color }) { return ( <button className="square" onClick={onSquareClick} style={{ backgroundColor: color }} > {value} </button> ); } function SquareSet({ xIsNext, squares, onPlay, winLine }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = "X"; } else { nextSquares[i] = "O"; } onPlay(nextSquares); } // 縦3列のマスを作成 const heights = [...Array(3).keys()].map((i) => i * 3); // 縦の開始Noに従って横3列を作成 return heights.map((height) => { const widths = [...Array(3).keys()].map((i) => height + i); const squareList = widths.map((i) => { const color = winLine && winLine.includes(i) ? "orange" : "white"; return ( <Square value={squares[i]} onSquareClick={() => handleClick(i)} color={color} key={i} /> ); }); return ( <div className="board-row" key={height}> {squareList} </div> ); }); } function Board({ xIsNext, squares, onPlay }) { const winLine = calculateWinner(squares); const status = winLine ? "Winner: " + (xIsNext ? "O" : "X") : squares.includes(null) ? "Next player: " + (xIsNext ? "X" : "O") : "引き分け!"; return ( <> <div className="status">{status}</div> <SquareSet xIsNext={xIsNext} squares={squares} onPlay={onPlay} winLine={winLine} /> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; const [isAsc, setIsAsc] = useState(true); function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(nextMove) { setCurrentMove(nextMove); } function changeOrder() { setIsAsc(!isAsc); } function createMoves() { let beforeSquare = [Array(9).fill(null)]; const moves = history.map((squares, move) => { const rowCal = findRowCal(beforeSquare, squares); beforeSquare = squares.slice(); const description = move > 0 ? "Go to move #" + move + "、設置場所:" + rowCal : "Go to game start"; if (move !== history.length - 1) { return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); } else { return ( <li key={move}> <div className="status">{"You are at move #" + move}</div> </li> ); } }); return isAsc ? moves : moves.reverse(); } const moves = createMoves(); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> <button onClick={() => changeOrder()}>昇順・降順切り替え</button> </div> </div> ); } function 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 lines[i]; } } return null; } function findRowCal(beforeSquare, nowSquare) { const rowCalSet = [ "row: 1, cal: 1", "row: 1, cal: 2", "row: 1, cal: 3", "row: 2, cal: 1", "row: 2, cal: 2", "row: 2, cal: 3", "row: 3, cal: 1", "row: 3, cal: 2", "row: 3, cal: 3" ]; const changeIndex = beforeSquare.findIndex( (x, index) => x === null && nowSquare[index] !== null ); return changeIndex < 0 ? "" : rowCalSet[changeIndex]; }
ChatGPTで改善点を聞いてみて回答結果のコード
- せっかくなのでChatGPTにコードを貼り付けて改善点を聞いてみた
挙がってきた改善点(改善例は下記のコード)
- Array.slice()をスプレッド構文([...Array])で行う
- 縦、横のマスを作成する処理を1関数に分離し、配列で実行するように変更
- 文字列結合を
~~ ${~~~}
で行う - 関数内で定義してる関数を外出しする(handleClick など)
大きく変わったのが2だけなので、全体としてはそこまで悪いコードではなかったと思いたい
- ちなみにこれをそのまま実行するとバグっており、正常動作しなかった
- 雑に指示しただけなので、もっと細かく指示すればちゃんと動くコードも作れそう
import { useState } from "react"; function Square({ value, onClick, color }) { return ( <button className="square" onClick={onClick} style={{ backgroundColor: color }} > {value} </button> ); } function BoardRow({ xIsNext, squares, onPlay, winLine }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = [...squares]; nextSquares[i] = xIsNext ? "X" : "O"; onPlay(nextSquares); } const renderSquare = (i) => { const color = winLine && winLine.includes(i) ? "orange" : "white"; return ( <Square value={squares[i]} onClick={() => handleClick(i)} color={color} key={i} /> ); }; return <div className="board-row">{[0, 1, 2].map(renderSquare)}</div>; } function Board({ xIsNext, squares, onPlay }) { const winLine = calculateWinner(squares); const status = winLine ? `Winner: ${xIsNext ? "O" : "X"}` : squares.includes(null) ? `Next player: ${xIsNext ? "X" : "O"}` : "引き分け!"; return ( <> <div className="status">{status}</div> {[0, 3, 6].map((start) => ( <BoardRow key={start} xIsNext={xIsNext} squares={squares.slice(start, start + 3)} onPlay={onPlay} winLine={winLine} /> ))} </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; const [isAsc, setIsAsc] = useState(true); function handlePlay(nextSquares) { setHistory((prevHistory) => [...prevHistory, nextSquares]); setCurrentMove((prevMove) => prevMove + 1); } function jumpTo(move) { setCurrentMove(move); } function changeOrder() { setIsAsc((prevIsAsc) => !prevIsAsc); } function createMoves() { let beforeSquare = Array(9).fill(null); const moves = history.map((squares, move) => { const rowCal = findRowCal(beforeSquare, squares); beforeSquare = squares.slice(); const description = move > 0 ? `Go to move #${move}、設置場所:${rowCal}` : "Go to game start"; if (move !== history.length - 1) { return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); } else { return ( <li key={move}> <div className="status">{"You are at move #" + move}</div> </li> ); } }); return isAsc ? moves : moves.reverse(); } const moves = createMoves(); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> <button onClick={changeOrder}>昇順・降順切り替え</button> </div> </div> ); } function 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 lines[i]; } } return null; } function findRowCal(beforeSquare, nowSquare) { const rowCalSet = [ "row: 1, cal: 1", "row: 1, cal: 2", "row: 1, cal: 3", "row: 2, cal: 1", "row: 2, cal: 2", "row: 2, cal: 3", "row: 3, cal: 1", "row: 3, cal: 2", "row: 3, cal: 3" ]; const changeIndex = beforeSquare.findIndex( (x, index) => x === null && nowSquare[index] !== null ); return changeIndex < 0 ? "" : rowCalSet[changeIndex]; }
最終的なコード
- ChatGPTの回答を参考に動くように一部修正したもの
- renderSquareの処理がスッキリして分かりやすくなった
- ただし、引数が
startIndex + i
になったりしていて3*3マスであることは分かりにくくなった印象 - コメント入れたり、配列作る時の数字を定数化すれば許容範囲かな、というところ
- 二重の三項演算子がある等、気になる点はまだあるが今回はこれを最終とする
import { useState } from "react"; function Square({ value, onClick, color }) { return ( <button className="square" onClick={onClick} style={{ backgroundColor: color }} > {value} </button> ); } function BoardRow({ xIsNext, squares, onPlay, winLine, startIndex }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = [...squares]; nextSquares[i] = xIsNext ? "X" : "O"; onPlay(nextSquares); } const renderSquare = (i) => { const color = winLine && winLine.includes(i) ? "orange" : "white"; return ( <Square value={squares[i]} onClick={() => handleClick(i)} color={color} key={i} /> ); }; return ( <div className="board-row"> {[...Array(3).keys()].map((i) => renderSquare(startIndex + i))} </div> ); } function Board({ xIsNext, squares, onPlay }) { const winLine = calculateWinner(squares); const status = winLine ? `Winner: ${xIsNext ? "O" : "X"}` : squares.includes(null) ? `Next player: ${xIsNext ? "X" : "O"}` : "引き分け!"; return ( <> <div className="status">{status}</div> {[...Array(3).keys()].map((start) => ( <BoardRow key={start} xIsNext={xIsNext} squares={squares} onPlay={onPlay} winLine={winLine} startIndex={start * 3} /> ))} </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; const [isAsc, setIsAsc] = useState(true); function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(move) { setCurrentMove(move); } function changeOrder() { setIsAsc(!isAsc); } function createMoves() { let beforeSquare = Array(9).fill(null); const moves = history.map((squares, move) => { const rowCal = findRowCal(beforeSquare, squares); beforeSquare = squares.slice(); const description = move > 0 ? `Go to move #${move}、設置場所:${rowCal}` : "Go to game start"; if (move !== history.length - 1) { return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); } else { return ( <li key={move}> <div className="status">{"You are at move #" + move}</div> </li> ); } }); return isAsc ? moves : moves.reverse(); } const moves = createMoves(); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> <button onClick={() => changeOrder()}>昇順・降順切り替え</button> </div> </div> ); } function 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 lines[i]; } } return null; } function findRowCal(beforeSquare, nowSquare) { const rowCalSet = [ "row: 1, cal: 1", "row: 1, cal: 2", "row: 1, cal: 3", "row: 2, cal: 1", "row: 2, cal: 2", "row: 2, cal: 3", "row: 3, cal: 1", "row: 3, cal: 2", "row: 3, cal: 3" ]; const changeIndex = beforeSquare.findIndex( (x, index) => x === null && nowSquare[index] !== null ); return changeIndex < 0 ? "" : rowCalSet[changeIndex]; }
Git Bash で OpenSSL を実行する方法
pem を pfx に変換する必要があり、方法を調べていたら Git Bash で openssl コマンドを実行すればいいらしいことが分かった。
いざコマンド実行してみたらエラーすら出ずに無反応になったので解決方法等をまとめ。
■簡易版
■詳細版
問題発生の流れ
- pem を pfx に変換するため、Git Bash で openssl コマンドを実行
- 「Enter Export Password」が表示される想定だが、何も表示されない
- 待機しても変化なし、処理されていない模様
$ openssl pkcs12 -export -in hoge.pem -inkey hogekey.pem -out hoge.pfx // 何も表示されず、処理も完了しない
原因
- Git インストール時「Configuring the terminal emulator to use with Git Bash」の設定で「Use MinTTY」を選択しているため
- MinTTY は WindowsAPI と互換性の問題があり、WindowsAPIを使用するプログラムを実行すると正常に動作しない
参考:MinTTY の Tips
https://github.com/mintty/mintty/wiki/Tips#inputoutput-interaction-with-alien-programs
解決方法
- コマンドの文頭に
winpty
を入力して実行する
$ winpty openssl pkcs12 -export -in hoge.pem -inkey hogekey.pem -out hoge.pfx Enter Export Password: Verifying - Enter Export Password: // 実行された
- 毎回入力するのが手間であれば、.bashrc にエイリアスを登録する
// .bashrc に以下を登録 ※~\.bashrcが無ければ手動で作成 alias openssl='winpty openssl' // ターミナルでコマンド実行 $ openssl pkcs12 -export -in hoge.pem -inkey hogekey.pem -out hoge.pfx Enter Export Password: Verifying - Enter Export Password: // winpty なしでも実行された
Git インストール時「Configuring the terminal emulator to use with Git Bash」の設定で「Use Windows' default console window」を選択する
※ターミナルがコマンドプロンプトになる
別の回避方法があるので、今回の問題のためだけに選択しなくてもいいのではと思うGit インストール時「Configuring experimental options」の設定で「Enable experimental support for pseudo consoles」にチェックを入れる
※注意書きにあるが、既知のバグがあるらしい
winpty とは?
- Windows コンソールプログラムと通信するためのインターフェースを提供するパッケージ
- MinTTY と WindowsAPI の間に入って非互換性を解決してくれる
参考:winpty の Readme
https://github.com/rprichard/winpty#readme