- 投稿日:2020-09-20T21:45:58+09:00
React 公式チュートリアルをtypescriptとredux、style-componentsを導入してやってみた [Typescript編]
テーマ
Reactチュートリアルをtypescriptとreduxを導入してみる
公式チュートリアル
https://ja.reactjs.org/tutorial/tutorial.htmltypescriptを入れてみた
順番
- reactにtypescriptを導入する
- reduxを導入する
- styled-componentsを導入する
環境構築
ローカル環境にprojectディレクトリを作成します。ここの下にreactチュートリアルのプロジェクトを作成していきます。
$ mkdir project $ cd projecttypescriptを導入するだけでしたらまず ここからForkするだけでも大丈夫かなと思います。
チュートリアル通りcreate-react-app でローカルで開発していきます。
$ npx create-react-app my-app --typescript $ cd my-appアプリが作成されたらアプリを開いて確認しましょう。
$ npm start
チュートリアルではファイル名のprefixは.jsxですが、以降はtypescriptなので.tsxで書いていきます。
こちらの記事通りに、
1. まず公式チュートリアルの通りに実装
2. TypeScriptに書き直すことでコンパイルエラーを解決
でやっていきます。
まず、create-react-app で生成された /src 以下のコードを全て削除して、0の状態に戻します。
$ rm -rf src/*次に、公式にある通りに src/index.css をコピペして追加します。
$ touch src/index.css $ touch src/index.tsx公式チュートリアルが用意してくれているindex.cssを貼る。
次に、src/index.tsx を追加して、公式通りに 同じく公式の用意しているindex.jsxをコピペします。
するとindex.tsxの以下の箇所がエラーを出すので以下のように修正する
/// Parameter 'i' implicitly has an 'any' type. renderSquare(i) -> renderSquare(i: number)これでコンパイルエラーが消えたので、また npm start すると成功します。
次にBoard の renderSquare メソッド内で、props として value という名前の値を Square に渡すようにコードを変更すると以下のエラーが出ます。
interfaceでプロパティの定義をします。
Property 'value' does not exist on type 'Readonly<{}> & Readonly<{ children?: ReactNode; }>'. TS2339
interface SquarePropsInterface { value: number; } class Square extends React.Component<SquarePropsInterface> {Squareコンポーネントがクリックされた場合に “X” と表示されるようにしましょう。
Squareクラスに下記を追加します。constructor(props) { super(props); this.state = { value: null, }; } <button className="square" onClick={() => this.setState({value: 'X'})}>するとこのエラーが出ます
Property 'value' does not exist on type 'Readonly<{}>'. TS2339
これはStateの定義にも Interface が必要ということです。以下のようにSquareクラスを修正します。
interface SquareStateInterface { value: string; } class Square extends React.Component<SquarePropsInterface, SquareStateInterface> { constructor(props:SquarePropsInterface) { super(props); this.state = { value: "", }; }ここまでのソースコード全体を載せておきます。
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; interface SquarePropsInterface { value: number; } interface SquareStateInterface { value: string; } class Square extends React.Component<SquarePropsInterface, SquareStateInterface> { constructor(props: SquarePropsInterface) { super(props); this.state = { value: "", }; } render() { return ( <button className="square" onClick={() => this.setState({value: 'X'})} > {this.state.value} </button> ); } } class Board extends React.Component { renderSquare(i: number) { return <Square value={i} />; } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } class Game extends React.Component { render() { return ( <div className="game"> <div className="game-board"> <Board /> </div> <div className="game-info"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') );ゲームを完成させる
ゲームの状態を各 Square の代わりに親の Board コンポーネントで保持する。
Board にコンストラクタを追加し、初期 state に 9 個の null が 9 個のマス目に対応する 9 個の null 値をセットinterface BoardPropsInterface { squares: Array<string> } interface BoardStateInterface { squares: Array<string> } class Board extends React.Component<BoardPropsInterface, BoardStateInterface> { constructor(props: BoardPropsInterface) { super(props); this.state = { squares: Array(9).fill(""), }; } renderSquare(i:number) { return <Square value={i} />; }props を渡すメカニズムを使うようにしましょう。Board を書き換えて、それぞれの個別の Square に現在の値('X'、'O' または null)を伝えるようにします。squares という配列は Board のコンストラクタで定義していますので、Board の renderSquare がそこから値を読み込むように書き換えましょう。
renderSquare(i:number) { return <Square value={this.state.squares[i]} />; }これでそれぞれの Square が value プロパティ('X'、'O'、または空のマス目の場合は null)を受け取るようになります。
次に、マス目がクリックされた時の挙動を変更しましょう。現在、どのマス目に何が入っているのかを管理しているのは Board です。Square が Board の state を更新できるようにする必要があります。state はそれを定義しているコンポーネント内でプライベートなものですので、Square から Board の state を直接書き換えることはできません。
代わりに、Board から Square に関数を渡すことにして、マス目がクリックされた時に Square にその関数を呼んでもらうようにしましょう。renderSquare メソッドを以下のように書き換えましょう:
renderSquare(i:number) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); }現在、Board から Square には props として 2 つの値を渡しています。value と onClick です。onClick プロパティはマス目がクリックされた時に Square が呼び出すためのものです。Square に以下のような変更を加えましょう。
Squareを関数コンポーネントで実装します。React における関数コンポーネントとは、render メソッドだけを有して自分の state を持たないコンポーネントを、よりシンプルに書くための方法です。React.Component を継承するクラスを定義する代わりに、props を入力として受け取り表示すべき内容を返す関数を定義します。関数コンポーネントはクラスよりも書くのが楽であり、多くのコンポーネントはこれで書くことができます。(Square を関数コンポーネントに変えた際、onClick={() => this.props.onClick()} をより短い onClick={props.onClick} に書き換えました)
- Square の render メソッド内の this.state.value をprops.value に書き換える
- Square の render メソッド内の this.setState() を props.onClick() に書き換える
- Square はもはやゲームの状態を管理しなくなったので、Square の constructor を削除する
interface SquarePropsInterface { value: string; onClick: () => void // } function Square(props: SquarePropsInterface) { return ( <button className="square" onClick={props.onClick} > {props.value} </button> ); }Square がクリックされると、Board から渡された onClick 関数がコールされます。どのようになっているのかおさらいしましょう。
- 組み込みの DOM コンポーネントである に onClick プロパティが設定されているため React がクリックに対するイベントリスナを設定します。
- ボタンがクリックされると、React は Square の render() メソッド内に定義されている onClick のイベントハンドラをコールします。
- このイベントハンドラが this.props.onClick() をコールします。Square の onClick プロパティは Board から渡されているものです。
- Board は Square に onClick={() => this.handleClick(i)} を渡していたので、Square はクリックされたときに this.handleClick(i) を呼び出します。
- まだ handleClick() は定義していないので、コードがクラッシュします。Square をクリックすると、“this.handleClick is not a function” といった赤いエラー画面が表示されるはずです。
この handleClick を Board クラスに加えましょう。
handleClick(i: number) { const squares: Array<string> = this.state.squares.slice(); squares[i] = 'X'; this.setState({ squares: squares }); }これらの変更を加えれば、再びマス目をクリックすると値が書き込まれるようになります。しかし今や、状態は個々の Square コンポーネントではなく Board コンポーネント内に保存されています。Board の state が変更になると、個々の Square コンポーネントも自動的に再レンダーされます。全てのマス目の状態を Board コンポーネント内で保持するようにしたことで、この後でどちらが勝者か判定できるようになります。
Square コンポーネントはもう自分で state を管理しないようになったので、Board コンポーネントから値を受け取って、クリックされた時はそのことを Board コンポーネントに伝えるだけになりました。React 用語でいうと、Square コンポーネントは制御されたコンポーネント (controlled component) になったということです。Board が Square コンポーネントを全面的に制御しています。
handleClick 内では、squares を直接変更する代わりに、.slice() を呼んで配列のコピーを作成していることに注意してください。なぜ squares 配列のコピーを作成しているのか説明します。
イミュータビリティは何故重要なのか
上記のコード例において、現在の配列を直接変更する代わりに、.slice() メソッドを使って square 配列のコピーを作成し、それを変更することをお勧めしました。ここでイミュータビリティ(immutability; 不変性)について解説し、それがなぜ重要なのかについて説明します。
一般的に、変化するデータに対しては 2 種類のアプローチがあります。1 番目のアプローチはデータの値を直接いじってデータをミューテート(mutate; 書き換え)することです。2 番目のアプローチは、望む変更を加えた新しいデータのコピーで古いデータを置き換えることです。
ミューテートを伴うデータの変化
var player = {score: 1, name: 'Jeff'}; player.score = 2; // Now player is {score: 2, name: 'Jeff'}ミューテートを伴わないデータの変化
var player = {score: 1, name: 'Jeff'}; var newPlayer = Object.assign({}, player, {score: 2}); // Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'} // Or if you are using object spread syntax proposal, you can write: // var newPlayer = {...player, score: 2};最終的な結果は同じですが、直接データのミューテート(すなわち内部データの書き換え)をしないことで、以下に述べるようないくつかの利点が得られます。
複雑な機能が簡単に実装できる
イミュータビリティにより、複雑な機能の実装がとても簡単になります。このチュートリアルの後の部分で、三目並べの着手の履歴を振り返って以前の着手まで「巻き戻し」ができる「タイムトラベル」機能を実装します。このような機能はゲーム特有のものではありません。直接的なデータのミューテートを避けることで、ゲームの以前のヒストリをそのまま保って後で再利用することができるようになります。
変更の検出
ミュータブル (mutable) なオブジェクトは中身が直接書き換えられるため、変更があったかどうかの検出が困難です。ミュータブルなオブジェクト変更の検出のためには、以前のコピーと比較してオブジェクトツリーの全体を走査する必要があります。
イミュータブルなオブジェクトでの変更の検出はとても簡単です。参照しているイミュータブルなオブジェクトが前と別のものであれば、変更があったということです。
React の再レンダータイミングの決定
イミュータビリティの主な利点は、React で pure component を構築しやすくなるということです。イミュータブルなデータは変更があったかどうか簡単に分かるため、コンポーネントをいつ再レンダーすべきなのか決定しやすくなります。
shouldComponentUpdate() および pure component をどのように作成するのかについては、パフォーマンス最適化のページで説明しています。
手番の処理
さて次に、この三目並べの明らかな欠点、すなわち “O” がまだ盤面に出てこないという問題を修正しましょう。
デフォルトでは、先手を “X” にします。Board のコンストラクタで state の初期値を変えればこのデフォルト値は変更可能です。
interface BoardPropsInterface { squares: Array<string> xIsNext: boolean } interface BoardStateInterface { squares: Array<string> xIsNext: boolean winner: string } class Board extends React.Component<BoardPropsInterface, BoardStateInterface> { constructor(props: BoardPropsInterface) { super(props); this.state = { squares: Array(9).fill(""), xIsNext: true, winner: "" }; }プレーヤが着手するたびに、どちらのプレーヤの手番なのかを決める xIsNext(真偽値)が反転され、ゲームの状態が保存されます。Board の handleClick 関数を書き換えて xIsNext の値を反転させるようにします。
handleClick(i:number) { const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; // <- ここを追加 this.setState({ squares: squares, xIsNext: !this.state.xIsNext, // <- ここを追加 }); }この変更により、“X” 側と “O” 側が交互に着手できるようになります。試してみてください!
Board の render 内にある “status” テキストも変更して、どちらのプレーヤの手番なのかを表示するようにしましょう。
render() { const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); return ( // the rest has not changedここまでの変更により、Board コンポーネントは以下のようになっているはずです。
Boardコンポーネント
class Board extends React.Component<BoardPropsInterface, BoardStateInterface> { constructor(props: BoardPropsInterface) { super(props); this.state = { squares: Array(9).fill(""), xIsNext: true, }; } handleClick(i: number) { const squares: Array<string> = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); } renderSquare(i: number) { return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />; } render() { let status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }ゲーム勝者の判定
どちらの手番なのかを表示できたので、次にやることはゲームが決着して次の手番がなくなった時にそれを表示することです。ファイル末尾に以下のヘルパー関数をコピーして貼り付けてください。
function calculateWinner(squares: Array<string>): string { 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 ""; }9 つの square の配列が与えられると、この関数は勝者がいるか適切に確認し、'X' か 'O'、あるいは null を返します。
Board の render 関数内で calculateWinner(squares) を呼び出して、いずれかのプレーヤが勝利したかどうか判定します。決着がついた場合は “Winner: X” あるいは “Winner: O” のようなテキストを表示するとよいでしょう。Board の render 関数の status 宣言を以下のコードで置き換えましょう。
render() { const winner = calculateWinner(this.state.squares); let status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); const winner = calculateWinner(this.state.squares); if (winner) { status = 'Winner: ' + winner; } return ( // the rest has not changedBoard の handleClick を書き換えて、ゲームの決着が既についている場合やクリックされたマス目が既に埋まっている場合に早期に return するようにします。
handleClick(i: number) { const winner = calculateWinner(this.state.squares); if (winner || this.state.squares[i]) { return; } const squares: Array<string> = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, winner: winner }); }おめでとうございます! これで動作する三目並べゲームができました。そして React の基本についても学ぶことができました。このゲームの真の勝者はあなたかもしれませんね。
ここまでのコードです。
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; interface SquarePropsInterface { value: string; onClick: () => void } function Square(props: SquarePropsInterface) { return ( <button className="square" onClick={props.onClick} > {props.value} </button> ); } interface BoardPropsInterface { squares: Array<string> xIsNext: boolean } interface BoardStateInterface { squares: Array<string> xIsNext: boolean winner: string } class Board extends React.Component<BoardPropsInterface, BoardStateInterface> { constructor(props: BoardPropsInterface) { super(props); this.state = { squares: Array(9).fill(""), xIsNext: true, winner: "" }; } handleClick(i: number) { const winner = calculateWinner(this.state.squares); if (winner || this.state.squares[i]) { return; } const squares: Array<string> = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, winner: winner }); } renderSquare(i: number) { return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />; } render() { let status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); const winner = calculateWinner(this.state.squares); if (winner) { status = 'Winner: ' + winner; } return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } class Game extends React.Component { render() { return ( <div className="game"> <div className="game-board"> <Board squares={Array(9).fill("")} xIsNext={true} /> </div> <div className="game-info"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') ); function calculateWinner(squares: Array<string>): string { 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 ""; }タイムトラベル機能の追加
最後の練習として、以前の着手まで「時間を巻き戻す」ことができるようにしましょう。
着手の履歴の保存
squares の配列をミューテートしていたとすれば、タイムトラベルの実装はとても難しかったでしょう。
しかし我々は着手があるたびに squares のコピーを作り、この配列をイミュータブルなものとして扱っていました。このため、squares の過去のバージョンをすべて保存しておいて、過去の手番をさかのぼることができるようになります。
過去の squares の配列を、history という名前の別の配列に保存しましょう。この history 配列は初手から最後までの盤面の全ての状態を表現しており、以下のような構造を持っています。
history = [ // Before first move { squares: [ null, null, null, null, null, null, null, null, null, ] }, // After first move { squares: [ null, null, null, null, 'X', null, null, null, null, ] }, // After second move { squares: [ null, null, null, null, 'X', null, null, null, 'O', ] }, // ... ]ここで、この history の状態をどのコンポーネントが保持すべきか考える必要があります。
State のリフトアップ、再び
トップレベルの Game コンポーネント内で過去の着手の履歴を表示したいと思います。そのためには Game コンポーネントが history にアクセスできる必要がありますので、history という state はトップレベルの Game コンポーネントに置くようにしましょう。
history state を Game コンポーネント内に置くことで、squares の state を、子である Board コンポーネントから取り除くことができます。Square コンポーネントにあった「state をリフトアップ」して Board コンポーネントに移動したときと全く同様にして、今度は Board にある state をトップレベルの Game コンポーネントにリフトアップしましょう。これにより Game コンポーネントは Board のデータを完全に制御することになり、history 内の過去の手番のデータを Board にレンダーさせることができるようになります。
まず、Game コンポーネントの初期 state をコンストラクタ内でセットします。
interface GameState { history: HistoryData[]; xIsNext: boolean; } class Game extends React.Component<{}, GameState> { constructor() { super(); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; // this.handleClick = this.handleClick.bind(this); }次に、Board コンポーネントが squares と onClick プロパティを Game コンポーネントから受け取るようにします。Board 内には多数のマス目に対応するクリックハンドラが 1 つだけあるので、Square の位置を onClick ハンドラに渡してどのマス目がクリックされたのかを伝えるようにします。以下の手順で Board コンポーネントを書き換えます。
- Board の constructor を削除する。
- Board の renderSquare にある this.state.squares[i] を this.props.squares[i] に置き換える。
- Board の renderSquare にある this.handleClick(i) を this.props.onClick(i) に置き換える。
Game コンポーネントの render 関数を更新して、ゲームのステータステキストの決定や表示の際に最新の履歴が使われるようにします。
render() { const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i) => this.handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{/* TODO */}</ol> </div> </div> ); }Game コンポーネントがゲームのステータステキストを表示するようになったので、対応するコードは Board 内の render メソッドからは削除できます。このリファクタリングの後で、Board の render 関数は以下のようになります。
render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); }最後に handleClick メソッドを Board コンポーネントから Game コンポーネントに移動します。また Game コンポーネントの state は異なる形で構成されていますので、handleClick の中身も修正する必要があります。Game 内の handleClick メソッドで、新しい履歴エントリを history に追加します。
handleClick(i:number) { const history = this.state.history; const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ history: history.concat([{ squares: squares, }]), xIsNext: !this.state.xIsNext, }); }現時点で Board コンポーネントに必要なのは renderSquare と render メソッドだけです。ゲームの状態と handleClick メソッドは Game コンポーネント内にあります。
過去の着手の表示
三目並べの履歴を記録しているので、これを過去の着手のリストとしてプレーヤに表示することが可能です。
以前、React 要素は第一級の JavaScript オブジェクトであり、それらをアプリケーション内で受け渡しできるということを学びました。React で複数の要素を描画するには、React 要素の配列を使うことができます。
JavaScript では、配列には map() メソッドが存在しており、これはデータを別のデータにマップするのによく利用されます。例えば:
const numbers = [1, 2, 3]; const doubled = numbers.map(x => x * 2); // [2, 4, 6]map メソッドを使うことで、着手履歴の配列をマップして画面上のボタンを表現する React 要素を作りだし、過去の手番に「ジャンプ」するためのボタンの一覧を表示できます。
Game の render メソッド内で history に map を作用させてみましょう。
const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); <ol>{moves}</ol>ゲームの履歴内にある三目並べのそれぞれの着手に対応して、ボタン を有するリストアイテム を作ります。ボタンには onClick ハンドラがあり、それは this.jumpTo() というメソッドを呼び出します。まだ jumpTo() は実装していません。ひとまずこのコードにより、ゲーム内で行われた着手のリストが表示されるようになりましたが、同時に開発者ツールのコンソール内に以下の警告も出力されているはずです:
Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.
この警告が何を意味するのかについて説明しましょう。
keyを選ぶ
リストをレンダーする際、リストの項目それぞれについて、React はとある情報を保持します。リストが変更になった場合、React はどのアイテムが変更になったのかを知る必要があります。リストのアイテムは追加された可能性も、削除された可能性も、並び替えられた可能性も、中身自体が変更になった可能性もあります。
例えば以下のツリーから:
<li>Alexa: 7 tasks left</li> <li>Ben: 5 tasks left</li>以下のツリーへ遷移する場合を想像してみてください:
<li>Ben: 9 tasks left</li> <li>Claudia: 8 tasks left</li> <li>Alexa: 5 tasks left</li>タスクの数も変わっていますが、これを人間が見た場合、おそらく Alexa と Ben の順番が変わって、その 2 人の間に Claudia が挿入されている、と考えるでしょう。しかし React は単なるコンピュータプログラムなので、あなたが意図するところを理解しません。React は我々の意図までは理解しないので、リストの項目それぞれに対して key プロパティを与えることで、兄弟要素の中でそのアイテムが区別できるようにしてあげる必要があります。このケースでは、alexa、ben、claudia の文字列を使う方法があります。データベースからのデータを表示している場合は、Alexa、Ben、Claudia のデータベース内での ID を key として使うこともできるでしょう。
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>リストが再レンダーされる際、React はそれぞれのリスト項目の key について、前回のリスト項目内に同じ key を持つものがないか探します。もし以前になかった key がリストに含まれていれば、React はコンポーネントを作成します。もし以前のリストにあった key が新しいリストに含まれていなければ、React は以前のコンポーネントを破棄します。もし 2 つの key がマッチした場合、対応するコンポーネントは移動されます。key はそれぞれのコンポーネントの同一性に関する情報を React に与え、それにより React は再レンダー間で state を保持できるようになります。もしコンポーネントの key が変化していれば、コンポーネントは破棄されて新しい state で再作成されます。
key は特別なプロパティであり React によって予約されています(より応用的な機能である ref も同様です)。要素が作成される際、React は key プロパティを引き抜いて、返される要素に直接その key を格納します。key は props の一部のようにも思えますが、this.props.key で参照できません。React はどの子要素を更新すべきかを決定する際に、key を自動的に使用します。コンポーネントが自身の key について確認する方法はありません。
動的なリストを構築する場合は正しい key を割り当てることが強く推奨されます。適切な key がない場合は、データ構造を再構成してそのような key が存在するようにするべきかもしれません。
key が指定されなかった場合、React は警告を表示し、デフォルトで key として配列のインデックスを使用します。配列のインデックスを key として使うことは、項目を並び替えたり挿入/削除する際に問題の原因となります。明示的に key={i} と渡すことで警告を消すことはできますが、配列のインデックスを使う場合と同様な問題が生じるためほとんどの場合は推奨されません。
key はグローバルに一意である必要はありません。コンポーネントとその兄弟の間で一意であれば十分です。
タイムトラベルの実装
三目並べゲームの履歴内においては、すべての着手にはそれに関連付けられた一意な ID が存在します。すなわち着手順の連番数字のことです。着手はゲームの最中に並び変わったり削除されたり挿入されたりすることはありませんから、着手のインデックスを key として使うのは安全です。
Game コンポーネントの render メソッド内で、key は
のようにして加えることができ、これで React の key に関する警告は表示されなくなります。const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); });まだ jumpTo メソッドが未定義なので、このリスト項目内のボタンをクリックするとエラーが発生します。jumpTo を実装する前に、Game コンポーネントの state に stepNumber という値を加えます。これは、いま何手目の状態を見ているのかを表すのに使います。
まず、Game の constructor 内で、state の初期値として stepNumber: 0 を加えます。
interface HistoryData { squares: ('O' | 'X' | null)[]; } interface GameState { history: HistoryData[]; xIsNext: boolean; stepNumber: number; } class Game extends React.Component<{}, GameState> { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], stepNumber: 0, xIsNext: true, }; }次に Game 内に jumpTo メソッドを定義してその stepNumber が更新されるようにします。また更新しようとしている stepNumber の値が偶数だった場合は xIsNext を true に設定します。
handleClick(i:number) { // this method has not changed } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); } render() { // this method has not changed }では次に、マス目をクリックしたときに実行される Game の handleClick メソッドに、いくつかの変更を加えます。
今加えた state である stepNumber は現在ユーザに見せている着手を反映しています。新しい着手が発生した場合は、this.setState の引数の一部として stepNumber: history.length を加えることで、stepNumber を更新する必要があります。
また this.state.history から読み取っているところを this.state.history.slice(0, this.state.stepNumber + 1) に書き換えます。これにより、「時間の巻き戻し」をしてからその時点で新しい着手を起こした場合に、そこから見て「将来」にある履歴(もはや正しくなくなったもの)を確実に捨て去ることができます。
handleClick(i: number) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; let squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ history: history.concat([{ squares, }]), stepNumber: history.length, xIsNext: !this.state.xIsNext, }); }最後に、Game コンポーネントの render を書き換えて、常に最後の着手後の状態をレンダーするのではなく stepNumber によって現在選択されている着手をレンダーするようにします。
render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); // the rest has not changedゲーム履歴内のどの手番をクリックした場合でも、三目並べの盤面は、該当の着手が発生した直後の状態を表示するように更新されるはずです。
完成コード
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; interface SquarePropsInterface { value: string; onClick: () => void } function Square(props: SquarePropsInterface) { return ( <button className="square" onClick={props.onClick} > {props.value} </button> ); } interface BoardPropsInterface { squares: Array<string> xIsNext: boolean, onClick: (i: number) => void; } interface BoardStateInterface { squares: Array<string> xIsNext: boolean winner: string } class Board extends React.Component<BoardPropsInterface, BoardStateInterface> { constructor(props: BoardPropsInterface) { super(props) } renderSquare(i: number) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } // type stage = { squares: Array<String>; } interface HistoryData { squares: Array<string> } interface gameProps {} interface GameState { history: HistoryData[]; xIsNext: boolean; stepNumber: number; } class Game extends React.Component<{}, GameState> { constructor(props:gameProps) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, stepNumber:0, }; } handleClick(i: number) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[this.state.stepNumber]; let squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ history: history.concat([{ squares, }]), stepNumber: history.length, xIsNext: !this.state.xIsNext, }); } jumpTo(step: number) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); } render() { const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move} > <button onClick={() => this.jumpTo(move)}> {desc}</button> </li> ) }); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : '○'); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i) => this.handleClick(i)} xIsNext /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') ); function calculateWinner(squares: Array<string>): string { 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 ""; }参考サイト
react-tutorial-starter
https://stackblitz.com/edit/react-ts-tutorial-starterTypeScriptを使ってReactのチュートリアルを進めると捗るかなと思った(実際捗る)
https://qiita.com/m0a/items/d723259cdeebe382b5f6React公式チュートリアルをTypeScriptでやる
https://note.com/tkugimot/n/nf7fe751298b1React のチュートリアルを Typescript でやってみた3(タイムトラベルの実装)【完】
https://qiita.com/humi/items/33e34183ccab3ff60a3b
- 投稿日:2020-09-20T13:21:29+09:00
Amplify SNS WorkshopでサーバレスなWebアプリ開発を手軽に実践
AWSのWebアプリケーション構築サービスであるAmplifyのワークショップ、
「Amplify SNS Workshop」をやってみたので紹介です。AWS Amplifyとは?
サービス紹介ページはこちら→https://aws.amazon.com/jp/amplify/
AWSの公式マンガでも紹介されています→https://aws.amazon.com/jp/campaigns/manga/vol3-1/AWS AmplifyはWebアプリの構築をサポートするサービス、ツール群です。
ReactやVueなどのモダンフレームワークを使用したフロントエンドに、バックエンドとしてAWSサービスを簡単につなぎこむことができます。Amplifyコンソールを使用して静的Webアプリケーションのホスティングをしたり、Amplifyフレームワークを使用してサーバレスなバックエンドの構築、フロントエンドとの接続をしたり、Webアプリ開発に必要な機能を包括しているサービスとなります。
本ワークショップにおいては、
- Reactアプリのホスティング
- AppSync, DynamoDBなどバックエンドの構築
- GitHub連携によるCI/CD環境構築
上記のようなWebアプリ開発の諸々のインタフェースとして使用します。
Amplify SNS Workshop
そんなAmplifyの実践入門として公開されているのが、「Amplify SNS Workshop」です。
こんな感じのTwitter風SNSアプリの開発を通して、AWSサービスを利用したバックエンドの構築、Reactアプリの開発、デプロイまで、Amplifyを使用したアプリ開発のワークフローを一通り実践することができます。構成は以下のようになっています。
一番下の人型がフロントエンドとなり、Reactで開発しています。ワークショップで使用しているAWSサービス
手順とかをここで説明しても意味がないので、本ワークショップのほうではあまり触れられていない、それぞれのサービスがどういうもので、本ワークショップではどういう役割なのか、について書いていきたいと思います。
AWS AppSync
概要
サービスページ→https://aws.amazon.com/jp/appsync/
これもマンガあった→https://aws.amazon.com/jp/campaigns/manga/vol4-4/GraphQLベースのWebAPIを作成できるサービスです。
サポートされているデータソースとしてはDynamoDB、Elasticsearch、Lambdaなどですが、直接サポートされていないRDSなどにもLambdaを介してアクセスすることができます。
GraphQLでサブスクリプションの定義をすることで、データソースの更新をリアルタイムにフロントエンドに反映することができるようになります。本ワークショップでの役割
- 直接DynamoDBからデータをREADするAPI(subscribe含む)
- Lambdaを介して複数のDynamoDBテーブルにデータをUPDATE, CREATEするAPI
- Elasticsearchにアクセスし、DynamoDBから取得したデータを全文検索するAPI
上記を作成、ホスティングするために使用します。
GraphQLのコーディング自体はローカルで行い、Amplifyフレームワークを利用してPUSHすることで、Amplifyを介してAppSyncに自動でリソースが作成されます。Amazon Cognito
概要
サービスページ→https://aws.amazon.com/jp/cognito/
Webアプリでの認証、認可、ユーザー管理をサポートするサービスです。
Cognitoで直接ユーザーを管理したり、サードバーティのソーシャルサインイン、SAMLなどによる管理も可能です。
AmplifyフレームワークとしてCognitoを使用した認証機能が提供されており、アプリに数行コードを追加するだけでログイン、サインイン、パスワード再発行などのUIとバックエンドが利用できるようになります。本ワークショップでの役割
ユーザー管理はすべてCognitoで行います。
UIもAmplifyフレームワークのReactコンポーネントを利用して作成しています↓import { withAuthenticator } from 'aws-amplify-react'; export default withAuthenticator(App);AWS Lambda
概要
サービスページ→https://aws.amazon.com/jp/lambda/
もはや解説不要のAWS主要サービス。
様々な言語で関数を作成し、サーバレスに動かすことができるサービスです。AppSyncではリゾルバとしてLambdaを選択することができ、
AppSyncからLambdaを呼び出してコードを実行することができます。本ワークショップでの役割
ワークショップの序盤では書き込みもすべてAppSyncから直接DynamoDBにアクセスしていました。
しかし、フォロイーユーザーのPOSTをフォロワーユーザーのHOMEタイムラインに表示させるという要件が追加されたため、
ユーザーがメッセージを投稿した際に、
- 投稿したユーザーのフォロワー検索
- フォロワーのタイムラインへのデータ更新
- 全体のPOSTテーブルへのデータ更新
以上を一度に行うためにLambda関数を利用しています。
Amazon Elasticsearch Service
概要
サービスページ→https://aws.amazon.com/jp/elasticsearch-service/
Elastic社のオープンソースの検索/分析エンジンであるElasticsearchをサポートするサービスです。
ログの分析、全文検索、データ分析などに利用されます。本ワークショップでの役割
ユーザーが投稿した全POSTの中から全文検索するためにAppSyncから呼び出しています。
GraphQLに定義を追加してAmplifyでPUSHするだけで構築してくれます。Amazon DynamoDB
概要
サービスページ→https://aws.amazon.com/jp/dynamodb/
サーバレスなNoSQLデータベースです。
高い可用性とスケーラビリティが売りの主要サービスです。
NoSQLデータベースのため、key-valueペアやドキュメントストレージとして利用します。本ワークショップでの役割
ユーザーのPOSTを保存します。
備考
料金について
AppSyncやLambdaなど、使用量に応じた従量課金なのでワークショップの途中で放置していてもほとんど料金はかからないと思います。
AmplifyでWebアプリをホストするにあたってS3にバケットを作成しているので、そこで少しかかるくらいかと。
また、無料期間中であれば従量課金系のサービスも無料枠内でワークショップが完結するものが多いです。
気になるようでしたら短期間にワークショップを終わらせましょう。
大体1,2日あれば最後までできます。後片付けについて
ワークショップのほうに書いてありますが、
amplify deleteでAppSync, DynamoDB, CloudFormation, Lambdaなど、ほとんどのリソースを削除できます。
Amplifyだけホストしているアプリが残っているようなので、コンソールからアプリを削除しましょう。やってみた感想
少し前からAmplifyでホスティングしたReactアプリの開発をしていましたが(どっちも未経験)、
Reactで一杯いっぱいになっていて、バックエンドについてはふわっとした理解しかできていませんでした。いや、今でもふわっとしているところはあるんですが、ワークショップを通じてGraphQL書いたり、Amplify CLIを叩いたりしていると、何となくバックエンド構築や、フロントエンドのとの繋ぎこみの雰囲気が分かりました。
あと単純にReactの勉強にもなった。
- 投稿日:2020-09-20T12:28:53+09:00
React Hooks 勉強 useMemo useCallback useRef
useMemo
プログラミングのテクニックにメモ化というものがあります。すごく大雑把に言うと
キャッシュの一種です。ある関数を呼び出した戻り値を保存しておき、同じ条件で呼び出す時には、実際に関数の中身を実行せず、保存してある戻り値を返すことで高速化します。
メモ化では副作用のあるものは取り扱いません。関数の引数が同じであれば結果が必ず同一になることと、その関数無いの処理で、I/O が生じたりするような副作用があると、メモ化というコンセプトに重大な問題が生じます。• 参照透過性(副作用が無い)
• 冪等性(同じ入力なら、出力は同じになる)useMemo は、副作用はないけど、何かしら演算しなければいけない処理を記述します。
functionコンポーネントのトップレベルの位置で以下の宣言を行いメモ化したい変数を定義します。第2引数にはuseEfectと同様に依存変数を配列で指定しておくことで依存変数に変更があった場合には再処理が実行されるようになります。
const メモ化したい変数 = useMemo(() => { // 実行したい処理を記述 return メモ化したい変数 },[依存変数を配列で記述])例えば受け取ったタイムスタンプを日付に変換している次のコンポーネントを見てみましょう。
const Foo = ({timestamp}) => { const dateObj = new Date(timestamp) const dateString = `${dateObj.getFullYear()}年${dateObj.getMonth() + 1}月${dateObj.getDate()}日` // YYYY年MM月DD日 return ( <p>日付:{dateString}</p> ) }上記のコードですと日付オブジェクトの作成や、日付オブジェクトから文字列を作成する処理がfunctionコンポーネントの再描画のたびに実行されてしまいます。
これをuseMemoを利用して書き直してみましょう。
const Foo = ({timestamp}) => { const dateString = useMemo(()=>{ const dateObj = new Date(timestamp) return `${dateObj.getFullYear()}年${dateObj.getMonth() + 1}月${dateObj.getDate()}日` },[timestamp]) return ( <p>日付:{dateString}</p> ) }useMemo内で日付文字列を生成するように変更しましたので、日付文字列を生成する処理は初回もしくはtimestampが変更された際にしか実行されなくなります。
useCallback
useCallback 関数は、useMemo 関数の亜種で、コールバックに使う関数をメモ化するためのものです。
React Hooks ではコンポーネント関数が何回も呼び出されるため、そこでコールバックのために関数定義をしようとすると、コンポーネント関数が実行されるたびに関数の生成が行われます。
関数が生成されると、その関数はたとえ処理が全く変わらなくても、以前の関数とは別物という扱いになります。これは、React では再レンダリングを巻き起こす可能性があること、React Hooks の処理上問題があります。useCallbackはメモ化したコールバック関数を返すHooks APIです。
コールバック関数を利用したイベント設定はReactのイベント設定で解説しましたが、useCallbackを利用しない場合はコールバック関数はfunctionコンポーネントの再描画のたびに新しい関数インスタンスを生成してイベントとしてバインドされていきます。const MyComponent = () => { // コールバック関数 const handleInput= (e) => { // イベント発生時に実行したい処理 console.log(e.target.value) } return ( <div> <input type="button" defaultValue="" onClick={handleInput}/> </div> ) }useCallbackを利用することで関数をメモ化して新しい関数インスタンスを生成せずに再描画後のイベントとして再利用を行います。
const MyComponent = () => { // コールバック関数 const handleInput= useCallback((e) => { // イベント発生時に実行したい処理 console.log(e.target.value) },[]) return ( <div> <input type="button" defaultValue="" onClick={handleInput}/> </div> ) }コールバック関数を作成する場合には極力useCallbackを利用するのがよいでしょう。
useRef
useRef 関数は、データを保持するために使われるもので、useState に似ていますが、useState とは違い、更新をしてもコンポーネント関数が再実行されたりはしません。
主な使い方は 2 つです。1 つめのケースは、DOM の参照を得るためのものです。
classコンポーネント利用時にはref属性を利用して要素を参照することができましたが、functionコンポーネントではuseRefを利用して要素の参照を行います。
次のサンプルではuseRefでinput要素への参照オブジェクトを作成してref属性でinput要素と関連付けを行っています。button要素がクリックされた際にinputEl.currentでinput要素のアクセスすることができます。function App() { const inputEl = useRef(null) const [text,changeText] = useState("") const handleClick = useCallback(()=>{ changeText(inputEl.current.value) },[]) return ( <> <p>text : {text}</p> <input ref={inputEl} type="text" /> <button onClick={handleClick}>set text</button> </> ) }同様のコードをuseRefを利用せずにuseStateを利用して記述した場合は以下のようになります。
function App() { const [tmpText,changeTmpText] = useState("") const [text,changeText] = useState("") const handleClick = useCallback(()=>{ changeText(tmpText) },[tmpText]) return ( <> <p>text : {text}</p> <input value={tmpText} onChange={e => changeTmpText(e.target.value)} type="text" /> <button onClick={handleClick}>set text</button> </> ) }入力中の文字列をステート「tmpText」に格納しておきボタンが押されたタイミングでステート「text」に代入することで同様の挙動を実装することができます。
useRefを利用するかどうかの大きな違いはコンポーネントの再描画が発生するかどうかです。
useRefを利用していないパターンではステート「tmpText」とステート「text」の更新時にコンポーネントの再描画が発生しますが、useRefを利用しているパターンではステート「text」の更新時にのみコンポーネントの再描画が発生します。
2つめのケースは、任意のデータを保持するためのものです。(値の参照)
Classコンポーネント時のref属性の利用方法や先程解説した要素へのアクセス方法からuseRefはDOMへのアクセスを保存するもののように思われがちですが、Classのインスタンス変数と同様な利用方法が可能です。
次のサンプルではuseRefを利用して変数「count」を作成してadd coutnボタンが押された際に1づつ加算していき、show logボタンが押されたタイミングでConsole上に現在の現在のカウントを表示することができるスクリプトです。
function App() { const count = useRef(0); const addCount = useCallback(()=>{ count.current += 1 },[]) const showLog = useCallback(()=>{ console.log(count.current) },[]) return ( <> <button onClick={addCount}>add count</button> <button onClick={showLog}>show log</button> </> ) }変数の初期値はuseRefの引数に指定ができ今回の初期値は0にしています。変数はcurrentプロパティに格納されているのでcount.currentに対して変更を行うことができ、呼び出し時も直接count.currentを呼び出します。
これをuseStateを利用して記述する場合は以下のようになります。
function App() { const [count, changeCount] = useState(0); const addCount = useCallback(()=>{ changeCount(prevCount => prevCount+1) },[]) const showLog = useCallback(()=>{ console.log(count) },[count]) return ( <> <button onClick={addCount}>add count</button> <button onClick={showLog}>show log</button> </> ) }const Sample: React.FC = () => { const ref = useRef(null) useEffect(() => { ref.current = .... }) }useState との違いは、値を更新したときに、関数が再度呼び出されるかどうかです。useState のステート更新関数を実行すると再度関数が呼び出されますが、useRef で作成した ref.current に値を代入しても、関数が再度呼び出されるトリガーにはなりません。
用途としては値を更新しても、いちいち再度関数が呼び出されたくないようなものを保持する場合に使います。クラス型コンポーネントでインスタンス変数に保持していたものを、置き換えるといいでしょう。こちらも大きな違いはコンポーネントの再描画が発生するかどうかです。useStateを利用している場合はステートcountの値に変更が発生する度にコンポーネントの再描画が発生しますがuseRefを利用している場合はコンポーネントの再描画が発生しません。
コンポーネントの再描画や子コンポーネントの再描画は行いたくないが内部の値のみ更新したい場合などでは値をuseStateで定義するのではなくuseRefを利用するのがよいでしょう。
useDebugValue
カスタムフックの中で、デバッグ用のデータをセットする為のものです。ライブラリを作成するなどの場合を除き、あまり使う事はないでしょう。
参考
- 投稿日:2020-09-20T12:07:50+09:00
React Hooks 勉強 useContext useReducer
useContext
階層が離れているコンポーネントに値を渡す為の Hooks 関数です。 まず React.createContext でコンテキストを作成します。
React.createContext()を利用することで、ステートの状態管理が可能なProviderコンポーネントが作成でき、value属性で管理したいステートを指定することができ、Provider内の子孫コンポーネントではuseContextを利用して管理しているステートにアクセスできます。import React, { createContext } from "react"; const context = createContext(""); const ConsumerSample = () => { return <context.Consumer>{text => <div>{text}</div>}</context.Consumer>; }; const ProvidertSample = () => { return ( <context.Provider value={"hoge"}> <ConsumerSample /> </context.Provider> ); }; export default ProvidertSample;Context は、createContext 関数で生成されます。この時、Context は、Provider と Consumer という 2 つのコンポーネントを持ちます。Provider コンポーネントは、value={}で引数を渡し、Consumer コンポーネントでは Provider の value を受け取るこ とができます。
この例だと、うれしい点は全く見当たりませんが、React のコンポーネントは、直接の子供に値を渡せますが、孫などの遠い子孫に値を渡すためにはバケツリレーのような面倒な方法をしなければ値を渡せません。
createContext で作成された Context のうち、Consumer コンポーネントさえ受け取 ることができれば、階層がどれだけ離れていても、Provider で提供された値を取得でき るというのが、Context のメリットです。
離れた階層でも値を受け取ることができる Consumer には 1 つ問題があります。<Consumer>{text => <div>{text}</div></Consumer>}という見慣れない記法は 一体何なのでしょうか? これは React の黒歴史で、render props というものです。 Hooks の登場により、この記法は廃絶されることでしょう。
import React, { createContext, useContext } from "react"; const context = createContext(""); const ConsumerSample = () => { const text = useContext(context); return <div>{text}</div>; }; const ProvidertSample = () => { return ( <context.Provider value={"hoge"}> <ConsumerSample /> </context.Provider> ); }; export default ProvidertSample;useReducer
useReducer 関数は Redux でおなじみの reducer を記述できる Hooks 関数です。
初期ステートを作成しておき、
const initialState = {count: 0};ステートを操作するためのreducer関数を作成します。
function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1} case 'decrement': return {count: state.count - 1} } }そしてfunctionコンポーネントのトップレベルの位置でuseReducerを利用して状態を保持したstateオブジェクトと状態を変更するためのreducer関数を作成します。
const [state, dispatch] = useReducer(reducer, initialState)こうすればstate.countとして現在のカウント数を増加させたり、dispatch({type: 'decrement'})という命令でカウントを減算させたり、dispatch({type: 'increment'})でカウントを加算することができます。
まとめると以下のように利用することができます。
import React, { useReducer } from 'react'; const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; } } export default function Foo() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }ただ、このような単純な状態管理はuseStateでも可能ですがuseReducerの利点は、ステートの更新ロジックをコンポーネントに非依存な外部のreducer関数に渡すことができる点です。
ロジックを分離
例えば以下のサンプルは入力されているテキストが0文字の場合もしくは15文字以上の場合はエラーを表示するというサンプルです。
const initialState = { text:'', error: 0 } function reducer(state, action) { switch (action.type) { case 'input': // error = 0:none 1:empty 2:overflow const error = action.payload.length === 0 ? 1 : action.payload.length > 15 ? 2 : 0 return { text: action.payload, error }; case 'reset': return initialState; } } function Foo() { const [state, dispatch] = useReducer(reducer, initialState); const handleChange = useCallback(e=>{ dispatch({type:'input',payload:e.target.value}) },[]) const handleReset = useCallback(()=>{ dispatch({type:'reset'}) },[]) return ( <> <input type="text" value={state.text} onChange={handleChange} /> <input type="button" value="reset" onClick={handleReset} /> {state.error === 1 && <p>空欄です</p>} {state.error === 2 && <p>15文字以上です</p>} </> ); }ロジックがreducer関数に埋め込まれているので、コンポーネントがシンプルになり、testなどの記述が容易になります。
また、依存する変数が少なくなるので、useCallbackやuseMemoによるパフォーマンスチューニングもしやすくなります。
管理する状態が複雑になりそうな場合はuseStateではなくuserReducerの利用を検討してみるのも良いでしょう。
さらにuserReducerはuseContextと組み合わせてコンポーネント間にまたがるステートを管理することができます。
参考
- 投稿日:2020-09-20T07:06:58+09:00
Webのフロントでコンポーネントを作る意味(コンポーネントライブラリへの感想)
0. 記事作成の動機:コンポーネントを作る煩わしさ
ReacやVueなどのJsでコンポーネントをつくるのがめんどくさいと感じるときが多々あった。
なので、コンポーネントを作る意味を再考してみる。以前、似たような記事を作ったけど、それをもう少し整理してみた。
SPA(Single Page Application) を採用するメリット再考(2020年)
1. コンポーネントを作ることの目的
knockoutjsを皮切りに10年以上SPAでのアプリを作ってきた。
最近感じるのは、VueやReactでのコンポーネント分割はすごく手間がかかるということ。
そこで改めて、コンポーネントを作ることを考えてみる。コンポーネントを作る目的は大きく分けて2つあると思う。
- 再利用
- コード分割(独立性)
以下、上記について補足。
(1) 再利用
コンポーネントを作ることの醍醐味であり、便利さを実感できるのがこの再利用性。
一度作ったコンポーネントをプロパティを替えていろいろな箇所で使える。
最近では、bit.devみたいなコンポーネントのリポジトリがあってそこで広く使えるようになると凄く便利。とはいえ、そこまで汎用性をもったコンポーネントというのはそうたくさんはできない。
どうしても汎用的に再利用しようとするとすごくシンプルなものになってくる。
そうすると、「わざわざつくらなくても、jQueryベースでもよくない?」となりかねない。プロジェクトでも、1度しか使わないようなコンポーネントがあったりしないだろうか?
では、1度しか使わないコンポーネントは要らないのか?
いや、そうではない。それが次の項目。(2) コード分割
コンポーネントのもう一つの側面は、ある目的や関心を小さく区切ってそれだけに焦点をあててコーディングすること。
そうすることで、コードの可読性があがったり、修正時の影響が極限化される。
これを踏まえた上で、いろいろなところで使われるようになると再利用性が実現される。とはいえ、1度しか使わないようなコンポーネントですら、上記のようなコーディング上のメリットがある。
2. JavascriptとHTML/CSSとの関係性:ライブラリの2つのグループ
コンポーネントライブラリを大きく分けると2つに分類できると思う。
- Javascript内にHTMLやCSSを内包する。(Javascriptベース型)
- Javascript、HTML、CSSを独立させるが一つのファイルで管理する。(分離パッケージ型)
以下、補足。
(1) Javascriptベース型
ReactやMithrillをベースにしたコンポーネントライブラリは、JSXによりJavascript(以下、Js)内にHTMLやCSSを内包する。そして、Jsベースでコンポーネントを作る。
(2) 分離パッケージ型
Angular、Vue、Svelte、Riotなどは、JsとHTML/CSSを分けて操作する。その代わり、HTMLに独自の属性(v-if)などを付与する。
3. ライブラリグループの評価
前節のグループのそれぞれの利点と問題点を指摘する。
(1) Javascriptベース型
a. 利点
Reactの場合、JsのクラスとHTML構造がマッピングされる。要は、HTMLのデータモデルをJsのクラスで表現する。
HTMLを扱う場合に、Js視点でコーディングでき、思考が1元化して分かりやすい。
また、HTMLとクラスをマッピングするという考え方により、コーディングルールが明確化し、コードの可読性が向上する。b. 問題点
ReactのようにJSをベースにすると、HTMLやCSSを別でコーダーさんがつくった場合、分割して、さらにJs内に取り込むという作業が発生する。
単なる分割は、分離パッケージ型でも発生するのだが、さらにJs内に取り込むという2段階の分割過程があり、意図通りに表示されないことがある。(2) 分離パッケージ型
a. 利点
Js、HTML、CSSをそれぞれ分離しながらも一つのコンポーネントファイル(たとえば、.vue)にまとめるので、関心が絞られて分かりやすい。また、js、HTML、CSSのコンテキスト(文脈)が別れているので読みやすく、コーダーさんが作ったものを分割するときも比較的手間が少ない。
タグ内に、独自属性を埋め込むことで、HTMLベースで構造ををプログラム化できるので、見た目を意識したい人には分かりやすい。ここは、Jsベースが良い人と好みが分かれるところかもしれない。
b. 問題点
HTMLやCSSをコーダーさんが作った場合分割する手間が発生する。
また、HTMLに独自属性を埋めていく手間が発生する。4. JavascriptとHTMLの関係:コンポーネントライブラリとjQuery
(1) 有名ライブラリはJsとHTMLが密接に依存する
上記にあげたライブラリを使うと、JsとHTMLは完全に独立しているわけでなく、HTMLを分割し、Jsに内包したり、HTMLに独自属性を付与するという依存性、手間が発生する。
そもそも、そうしないとコンポーネントはつくれないだろうか?
そんなことはない。(2) HTMLを壊さずに操作するjQueryの視点
jQueryはコンポーネントを作るためのライブラリではない。あくまでもHTML(DOM)を手軽に操作するシンタックスシュガー、ユーティリティライブラリである。
ただ、このライブラリの観点は、HTMLをReactのようなJsベース型のように内側から操作するのではなく、外側から操作する。
また、Vueのような分離パッケージ型のように独自属性も挿入しない。
つまり、jQueryは、HTMLと完全に独立した存在で、HTMLを変更せず、完全に外側から操作する。
コーダーさんがつくったコードを分割することも、変更することもなく操作することができる。(3) jQueryの欠点:Js内の値(モデル)とHTML(DOM)の関係性が曖昧、不定
jQueryは前述したとおり、コンポーネントをつくることを目的にはしていない。
なので、書き手によっていろいろな操作コードが書かれることになり、可読性が必ずしも保たれない。ReactのようにJsとHTMLの関係性が明確であれば、コードリーディングは用意であるが、jQueryの場合そのルールがないので、コードリーディンが必ずしも容易とはならない。
特に、データモデルというJs内でのデータとDOMとの関係性をどうつくるかは明示されないので、書き手任せになってしまう。
ここは、ReactのようなJsベース型ライブラリの方が優位である。
5. JsとHTMLを独立化させる利点
jQueryにはJsとの関係性が曖昧、不定になるという欠点はあったが、JsがHTMLと独立化するというのは利点もある。
(1) 構造と振る舞いを独立し、異なるデザインに対応
自社サービスなどでは、デザインやHTMLの構造がそう大きく変わることはないが、受託開発の場合、デザインもHTML、CSSも案件ごとに変わることがある。
それを自社開発したコンポーネントに合わせて変換させていくこともできるが、やはり分割や調整の手間が発生する。JsとHTMLが依存関係になければこのようなことはあまり発生しない。適宜、HTMLにidやclassなどセレクターを打ち直すか、Js側で指定するセレクターを変えるだけでよい。
(2) JsとHTML/CSSの技術的な変化速度の違い
Jsの技術変化の速度は速い。一方、HTMLやCSSの仕様が変化される速度はとても遅い。つまり、JsとHTMLが依存化している場合、Jsの変更にあわせてHTMLやCSSも変更させる可能性がある。
JsとHTMLを独立しておけば、Jsが変わろうともHTMLとCSSの構造やスタイルは変更することは少ない。6. JsとHTMLを独立させつつ対応付け
(1) ReactとjQueryの視点を合わせると良いのでは?
ここまでにみてきたとおり、有名なコンポーネントライブラリはHTMLとの依存性が高い。けれど、それは表裏一体で、HTMLとJsとの関係性が分かりやすくもある。
ただ、ソースコードとして、それらを混ぜ合わせておく必要があるだろうか?
コードのルールとしてそれがあればよいのではないだろうか?
そうすることで、JsとHTMLの依存による問題も起きないが、コードリーディングもしやすくなる。
つまり、ReactのようなDOMをJsのクラスで表現しつつ、jQueryのようにHTMLに変更を加えず、操作する方法であれば、可読性と独立性が保たれる。ただし、VueのようにHTMLに属性をうって、HTMLをみながら構造を操作するのが好きな人は直感性が失われるかもしれない。
ただ、コーダーさんが作ったものを分割する手間は減るので、かなり効率性はあがると思う。
ちなみに、外側からDOMを対応させるクラスを作るのであればAngularのComponentクラスがよいと思う。クラスのプロパティにDOMを指示するselectorを持ったせて、対応づけをするのは良いと思う。
Angular公式:Introduction to components and templates
componentexport class HeroListComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor(private service: HeroService) { } ngOnInit() { this.heroes = this.service.getHeroes(); } selectHero(hero: Hero) { this.selectedHero = hero; } }(2) Simulacra.js:JsとHTMLを独立化させたデータバインディングライブラリ
このような考えができるライブラリとしては、以下のライブラリがある。要は、SPAでいうところのデータバインディングの問題で、HTMLを変更しないデータバインディングライブリである。
ただし、双方向バインディングではなく一方向だけである。そもそも、双方向である必要はいつもあるとは限らない。
JsでHTMLのセレクターをキーとするオブジェクトリテラルをつくり、ライブラリに渡すと、データバインディングしてくれる。
template<template id="product"> <h1 class="name"></h1> <div class="details"> <div><span class="size"></span></div> <h4 class="vendor"></h4> </div> </template>上記のタグに対応するモデルとなるオブジェクトリテラルを作る。
modelvar state = { name: 'Pumpkin Spice Latte', details: { size: [ 'Tall', 'Grande', 'Venti' ], vendor: 'Coffee Co.' } }
バインディング。bindvar bindObject = require('simulacra') // or `window.simulacra` var template = document.getElementById('product') var node = bindObject(state, [ template, { name: '.name', details: [ '.details', { size: '.size', vendor: '.vendor' } ] } ]) document.body.appendChild(node)個人的に、全体的な視点や発想はいいと思うが、モデルとセレクタのバインディングコードが無駄な気がしている。
この部分もなくすことができる。(自作したことがある)(3) eleventy.js(静的サイトジェネレーター):HTMLの再利用
JsとHTMLを分けると、HTMLを再利用するときどうするのかというと、シンプルに静的サイトジェネレーターのinclude機能を使えばいいと思います。
オススメの静的サイトジェネレーターはJsで動く「11ty:イレブンティ」。
デプロイ前に動的に作りたいなら、11tyでJsのフィルター関数を作ってしまえばいいと思います。
コーディングが楽です。わざわざGatsbyみたいにJsでコンポーネント作らなくても、pug、nanjucks、markdown、htmlなどでHTMLを作れます。
もちろん、フィルター関数とかも作れるので、繰り返し作業とか動的な処理も対応できます。7. まとめ:コンポーネントという「発想・視点」は大切
(1) コンポーネントライブラリを使う煩わしさ
この記事を書いた動機は、受託をしていると毎回デザインがかわり、HTMLやCSSも変わる事が多く、その都度、コンポーネントを別途つくる手間が凄くめんどくさかった。
また、わざわざコーディングしてあるHTMLをバラすという手間も無駄に感じた。
その他、VuexやReduxなどの状態管理が煩わしく、そもそも双方向バインディングなどをせず、ワントランザクションでSPAをつくれば、状態も複雑化しないので、使う必要をあまり感じなかった。
というように、コンポーネントという「考え方・設計」はいいのだけど、その実現方法について無駄や手間を感じたので、こうした記事を書いてみた。
(2) コンポーネントライブラリを使う場所(自社サービス・大規模開発)
利用シーンによっては煩わしいのだけど、自社サービスの大規模開発の場合は、ReactやVueなどの有名なコンポーネントライブラリを使うのがよいと思う。
その方がドキュメントの整備も公式に任せればいいし、ネット上に情報が沢山ある。またコーディングを規制することができるので、書き方をプログラマー間で統一させやすい。
自社サービスの場合、デザインやHTMLの構造などもコンポーネント側に合わせた開発できる。裏返せば、受託で毎回デザインが変わる場合、必ずしもコンポーネントライブラリを使うべきかは考えようだと思う。
以下に分かりやすいコーディング設計ができるかに依存する。でも、それができないと考えるなら、有名なライブラリを使うのが無難だと思う。
(3) コンポーネントは「発想・視点」が大切
コードを独立化、局所化するという発想がコンポーネントはよいと思う。
これは疑義なく支持したい。
けれど、その実現方法については、ケースバイケースかなという気もする。
自社サービスか受託開発、大規模か小規模か、などプロジェクトの前提条件によっても異なると思う。とにかくコンポーネントを作る手間はどんなものでも発生するのだけど、できるだけその手間が最小化するものがよいと思う。
- 投稿日:2020-09-20T07:06:58+09:00
Webのフロントでコンポーネントを作る意味(JsとHTMLの依存性、コンポーネントライブラリへの感想)
0. 記事作成の動機:コンポーネントを作る煩わしさ
ReacやVueなどのJsでコンポーネントをつくるのがめんどくさいと感じるときが多々あった。
なので、コンポーネントを作る意味を再考してみる。以前、似たような記事を作ったけど、それをもう少し整理してみた。
SPA(Single Page Application) を採用するメリット再考(2020年)
1. コンポーネントを作ることの目的
knockoutjsを皮切りに10年以上SPAでのアプリを作ってきた。
最近感じるのは、VueやReactでのコンポーネント分割はすごく手間がかかるということ。
そこで改めて、コンポーネントを作ることを考えてみる。コンポーネントを作る目的は大きく分けて2つあると思う。
- 再利用
- コード分割(独立性)
以下、上記について補足。
(1) 再利用
コンポーネントを作ることの醍醐味であり、便利さを実感できるのがこの再利用性。
一度作ったコンポーネントをプロパティを替えていろいろな箇所で使える。
最近では、bit.devみたいなコンポーネントのリポジトリがあってそこで広く使えるようになると凄く便利。とはいえ、そこまで汎用性をもったコンポーネントというのはそうたくさんはできない。
どうしても汎用的に再利用しようとするとすごくシンプルなものになってくる。
そうすると、「わざわざつくらなくても、jQueryベースでもよくない?」となりかねない。プロジェクトでも、1度しか使わないようなコンポーネントがあったりしないだろうか?
では、1度しか使わないコンポーネントは要らないのか?
いや、そうではない。それが次の項目。(2) コード分割
コンポーネントのもう一つの側面は、ある目的や関心を小さく区切ってそれだけに焦点をあててコーディングすること。
そうすることで、コードの可読性があがったり、修正時の影響が極限化される。
これを踏まえた上で、いろいろなところで使われるようになると再利用性が実現される。とはいえ、1度しか使わないようなコンポーネントですら、上記のようなコーディング上のメリットがある。
2. JavascriptとHTML/CSSとの関係性:ライブラリの2つのグループ
コンポーネントライブラリを大きく分けると2つに分類できると思う。
- Javascript内にHTMLやCSSを内包する。(Javascriptベース型)
- Javascript、HTML、CSSを独立させるが一つのファイルで管理する。(分離パッケージ型)
以下、補足。
(1) Javascriptベース型
ReactやMithrillをベースにしたコンポーネントライブラリは、JSXによりJavascript(以下、Js)内にHTMLやCSSを内包する。そして、Jsベースでコンポーネントを作る。
(2) 分離パッケージ型
Angular、Vue、Svelte、Riotなどは、JsとHTML/CSSを分けて操作する。その代わり、HTMLに独自の属性(v-if)などを付与する。
3. ライブラリグループの評価
前節のグループのそれぞれの利点と問題点を指摘する。
(1) Javascriptベース型
a. 利点
Reactの場合、JsのクラスとHTML構造がマッピングされる。要は、HTMLのデータモデルをJsのクラスで表現する。
HTMLを扱う場合に、Js視点でコーディングでき、思考が1元化して分かりやすい。
また、HTMLとクラスをマッピングするという考え方により、コーディングルールが明確化し、コードの可読性が向上する。b. 問題点
ReactのようにJsをベースにすると、HTMLやCSSを別でコーダーさんがつくった場合、分割して、さらにJs内に取り込むという作業が発生する。
単なる分割は、分離パッケージ型でも発生するのだが、さらにJs内に取り込むという2段階の分割過程があり、意図通りに表示されないことがある。(2) 分離パッケージ型
a. 利点
Js、HTML、CSSをそれぞれ分離しながらも一つのコンポーネントファイル(たとえば、.vue)にまとめるので、関心が絞られて分かりやすい。また、Js、HTML、CSSのコンテキスト(文脈)が別れているので読みやすく、コーダーさんが作ったものを分割するときも比較的手間が少ない。
タグ内に、独自属性を埋め込むことで、HTMLベースで構造ををプログラム化できるので、見た目を意識したい人には分かりやすい。ここは、Jsベースが良い人と好みが分かれるところかもしれない。
b. 問題点
HTMLやCSSをコーダーさんが作った場合分割する手間が発生する。
また、HTMLに独自属性を埋めていく手間が発生する。4. JavascriptとHTMLの関係:コンポーネントライブラリとjQuery
(1) 有名ライブラリはJsとHTMLが密接に依存する
上記にあげたライブラリを使うと、JsとHTMLは完全に独立しているわけでなく、HTMLを分割し、Jsに内包したり、HTMLに独自属性を付与するという依存性、手間が発生する。
そもそも、そうしないとコンポーネントはつくれないだろうか?
そんなことはない。(2) HTMLを壊さずに操作するjQueryの視点
jQueryはコンポーネントを作るためのライブラリではない。あくまでもHTML(DOM)を手軽に操作するシンタックスシュガー、ユーティリティライブラリである。
ただ、このライブラリの観点は、HTMLをReactのようなJsベース型のように内側から操作するのではなく、外側から操作する。
また、Vueのような分離パッケージ型のように独自属性も挿入しない。
つまり、jQueryは、HTMLと完全に独立した存在で、HTMLを変更せず、完全に外側から操作する。
コーダーさんがつくったコードを分割することも、変更することもなく操作することができる。(3) jQueryの欠点:Js内の値(モデル)とHTML(DOM)の関係性が曖昧、不定
jQueryは前述したとおり、コンポーネントをつくることを目的にはしていない。
なので、書き手によっていろいろな操作コードが書かれることになり、可読性が必ずしも保たれない。ReactのようにJsとHTMLの関係性が明確であれば、コードリーディングは容易であるが、jQueryの場合そのルールがないので、コードリーディンが必ずしも容易とはならない。
特に、データモデルというJs内でのデータとDOMとの関係性をどうつくるかは明示されないので、書き手任せになってしまう。
ここは、ReactのようなJsベース型ライブラリの方が優位である。
5. JsとHTMLを独立化させる利点
jQueryにはJsとの関係性が曖昧、不定になるという欠点はあったが、JsがHTMLと独立化するというのは利点もある。
(1) 構造と振る舞いを独立し、異なるデザインに対応
自社サービスなどでは、デザインやHTMLの構造がそう大きく変わることはないが、受託開発の場合、デザインもHTML、CSSも案件ごとに変わることがある。
それを自社開発したコンポーネントに合わせて変換させていくこともできるが、やはり分割や調整の手間が発生する。JsとHTMLが依存関係になければこのようなことはあまり発生しない。適宜、HTMLにidやclassなどセレクターを打ち直すか、Js側で指定するセレクターを変えるだけでよい。
(2) JsとHTML/CSSの技術的な変化速度の違い
Jsの技術変化の速度は速い。一方、HTMLやCSSの仕様が変化される速度はとても遅い。つまり、JsとHTMLが依存化している場合、Jsの変更にあわせてHTMLやCSSも変更させる可能性がある。
JsとHTMLを独立しておけば、Jsが変わろうともHTMLとCSSの構造やスタイルは変更することは少ない。6. JsとHTMLを独立させつつ対応付け
(1) ReactとjQueryの視点を合わせると良いのでは?
ここまでにみてきたとおり、有名なコンポーネントライブラリはHTMLとの依存性が高い。けれど、それは表裏一体で、HTMLとJsとの関係性が分かりやすくもある。
ただ、ソースコードとして、それらを混ぜ合わせておく必要があるだろうか?
コードのルールとしてそれがあればよいのではないだろうか?
そうすることで、JsとHTMLの依存による問題も起きないが、コードリーディングもしやすくなる。
つまり、ReactのようなDOMをJsのクラスで表現しつつ、jQueryのようにHTMLに変更を加えず、操作する方法であれば、可読性と独立性が保たれる。ただし、VueのようにHTMLに属性をうって、HTMLをみながら構造を操作するのが好きな人は直感性が失われるかもしれない。
ただ、コーダーさんが作ったものを分割する手間は減るので、かなり効率性はあがると思う。
ちなみに、外側からDOMを対応させるクラスを作るのであればAngularのComponentクラスがよいと思う。クラスのプロパティにDOMを指示するselectorを持ったせて、対応づけをするのは良いと思う。
Angular公式:Introduction to components and templates
componentexport class HeroListComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor(private service: HeroService) { } ngOnInit() { this.heroes = this.service.getHeroes(); } selectHero(hero: Hero) { this.selectedHero = hero; } }(2) Simulacra.js:JsとHTMLを独立化させたデータバインディングライブラリ
このような考えができるライブラリとしては、以下のライブラリがある。要は、SPAでいうところのデータバインディングの問題で、HTMLを変更しないデータバインディングライブリである。
ただし、双方向バインディングではなく一方向だけである。そもそも、双方向はいつも必要だろうか?
JsでHTMLのセレクターをキーとするオブジェクトリテラルをつくり、ライブラリに渡すと、データバインディングしてくれる。
template<template id="product"> <h1 class="name"></h1> <div class="details"> <div><span class="size"></span></div> <h4 class="vendor"></h4> </div> </template>上記のタグに対応するモデルとなるオブジェクトリテラルを作る。
modelvar state = { name: 'Pumpkin Spice Latte', details: { size: [ 'Tall', 'Grande', 'Venti' ], vendor: 'Coffee Co.' } }
バインディング。bindvar bindObject = require('simulacra') // or `window.simulacra` var template = document.getElementById('product') var node = bindObject(state, [ template, { name: '.name', details: [ '.details', { size: '.size', vendor: '.vendor' } ] } ]) document.body.appendChild(node)個人的に、全体的な視点や発想はいいと思うが、モデルとセレクタのバインディングコードが無駄な気がしている。
この部分もなくすことができる。(自作したことがある)(3) eleventy.js(静的サイトジェネレーター):HTMLの再利用
JsとHTMLを分けると、HTMLを再利用するときどうするのかというと、シンプルに静的サイトジェネレーターのinclude機能を使えばいいと思います。
オススメの静的サイトジェネレーターはJsで動く「11ty:イレブンティ」。
デプロイ前に動的に作りたいなら、11tyでJsのフィルター関数を作ってしまえばいいと思います。
コーディングが楽です。わざわざGatsbyみたいにJsでコンポーネント作らなくても、pug、nanjucks、markdown、htmlなどでHTMLを作れます。
もちろん、フィルター関数とかも作れるので、繰り返し作業とか動的な処理も対応できます。7. まとめ:コンポーネントという「発想・視点」は大切
(1) コンポーネントライブラリを使う煩わしさ
この記事を書いた動機は、受託をしていると毎回デザインがかわり、HTMLやCSSも変わる事が多く、その都度、コンポーネントを別途つくる手間が凄くめんどくさかった。
また、わざわざコーディングしてあるHTMLをバラすという手間も無駄に感じた。
その他、VuexやReduxなどの状態管理が煩わしく、そもそも双方向バインディングなどをせず、ワントランザクションでSPAをつくれば、状態も複雑化しないので、使う必要をあまり感じなかった。
というように、コンポーネントという「考え方・設計」はいいのだけど、その実現方法について無駄や手間を感じたので、こうした記事を書いてみた。
(2) コンポーネントライブラリを使う場所(自社サービス・大規模開発)
利用シーンによっては煩わしいのだけど、自社サービスの大規模開発の場合は、ReactやVueなどの有名なコンポーネントライブラリを使うのがよいと思う。
その方がドキュメントの整備も公式に任せればいいし、ネット上に情報が沢山ある。またコーディングを規制することができるので、書き方をプログラマー間で統一させやすい。
自社サービスの場合、デザインやHTMLの構造などもコンポーネント側に合わせた開発できる。裏返せば、受託で毎回デザインが変わる場合、必ずしもコンポーネントライブラリを使うべきかは考えようだと思う。
以下に分かりやすいコーディング設計ができるかに依存する。でも、それができないと考えるなら、有名なライブラリを使うのが無難だと思う。
(3) コンポーネントは「発想・視点」が大切
コードを独立化、局所化するという発想がコンポーネントはよいと思う。
これは疑義なく支持したい。
けれど、その実現方法については、ケースバイケースかなという気もする。
自社サービスか受託開発、大規模か小規模か、などプロジェクトの前提条件によっても異なると思う。とにかくコンポーネントを作る手間はどんなものでも発生するのだけど、できるだけその手間が最小化するものがよいと思う。
- 投稿日:2020-09-20T03:11:28+09:00
[MERN⑧] Dashboard & Profile Management
~~~~~~~~~~ (Contents) MERN ~~~~~~~~~~~
[MERN①] Express & MongoDB Setup
https://qiita.com/niyomong/private/3281af84486876f897f7
[MERN②]User API Routes & JWT Authentication
https://qiita.com/niyomong/private/c11616ff7b64925f9a2b
[MERN③] Profile API Routes
https://qiita.com/niyomong/private/8cff4e6fa0e81b92cb49
[MERN④] Post API
https://qiita.com/niyomong/private/3ce66f15375ad04b8989
[MERN⑤] Getting Started With React & The Frontend
https://qiita.com/niyomong/private/a5759e2fb89c9f222b6b
[MERN⑥] Redux Setup & Alerts
https://qiita.com/niyomong/private/074c27259924c7fd306b
[MERN⑦] React User Authentication
https://qiita.com/niyomong/private/37151784671eff3b92b6
[MERN⑧] Dashboard & Profile Management
https://qiita.com/niyomong/private/ab7e5da1b1983a226aca
[MERN⑨] Profile Display
https://qiita.com/niyomong/private/42426135e959c7844dcb
[MERN⑩] Posts & Comments
https://qiita.com/niyomong/private/19c78aea482b734c3cf5
[MERN11] デプロイ
https://qiita.com/niyomong/private/150f9000ce51548134ad
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1. Protected Route For Dashboard
① dashboardフォルダ生成 -> Dashboardコンポーネントファイル生成
dashboard.js//rafcp(ショートカット) -> 'Enter' import React from 'react'; import PropTypes from 'prop-types'; const Dashboard = (props) => { return <div>Dashboard</div>; }; Dashboard.propTypes = {}; export default Dashboard;② PrivateRoute設定 + App.jsでPrivateRoute設置
~~ 詳細な説明 ~~
(1) 小文字のcomponent propsを大文字のComponent変数に代入。
-> 残りのpropsは全て...rest
に格納
(2) App.jsが受け入れているRoute
を呼び出す。
・未認証 AND 未lodingの場合は、/login
に遷移
・認証済みの場合は、App.jsのPrivateRouteの先のComponent(例;Dashboardコンポ)に遷移。components/routing/PrivateRoute.jsimport React, { Component } from 'react'; import { Route, Redirect } from 'react-router-dom'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; const PrivateRoute = ({ (1) component: Component, auth: { isAuthenticated, loading }, (1) ...rest }) => ( <Route (2) {...rest} (2) render={(props) => (2) !isAuthenticated && !loading ? ( (2) <Redirect to="/login" /> (2) ) : ( (2) <Component {...props} /> ) } /> ); PrivateRoute.propTypes = { auth: PropTypes.object.isRequired, }; const mapStateToProps = (state) => ({ auth: state.auth, }); export default connect(mapStateToProps)(PrivateRoute);components/dashboard/Dashboard.js//... import Alert from './components/layout/Alert'; + import Dashboard from './components/dashboard/Dashboard'; + import PrivateRoute from './components/routing/PrivateRoute'; //Redux //... return ( //... <Switch> <Route exact path="/register" component={Register} /> <Route exact path="/login" component={Login} /> + <PrivateRoute exact path="/dashboard" component={Dashboard} /> </Switch> //...2. Profile Reducer & Get Current Profile
① PROFILE TYPE追加
actions/types.js+ export const GET_PROFILE = 'GET_PROFILE'; + export const PROFILE_ERROR = 'PROFILE_ERROR';② ProfileReducer生成
reducers/profile.jsimport { GET_PROFILE, PROFILE_ERROR } from '../actions/types'; const initialState = { profile: null, profiles: [], //profile listingページ用 repos: [], //不要かも。。。 loading: true, error: {}, }; export default function (state = initialState, action) { const { type, payload } = action; switch (type) { case GET_PROFILE: return { ...state, profile: payload, loading: false, }; case PROFILE_ERROR: return { ...state, error: payload, loading: false, profile: null }; default: return state; } }③ Profileアクション
actions/profile.jsimport axios from 'axios'; import { setAlert } from './alert'; import { GET_PROFILE, PROFILE_ERROR } from './types'; // Get current users profile export const getCurrentProfile = () => async (dispatch) => { try { const res = await axios.get('/api/profile/me'); dispatch({ type: GET_PROFILE, payload: res.data, }); } catch (err) { dispatch({ type: PROFILE_ERROR, payload: { msg: err.response.statusText, status: err.response.status }, }); } };④ Dashboardコンポーネント
components/dashboard/Dashboard.jsimport React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { getCurrentProfile } from '../../actions/profile'; const Dashboard = ({ getCurrentProfile, auth, profile }) => { useEffect(() => { getCurrentProfile(); }, [getCurrentProfile]); return <div>Dashboard</div>; }; Dashboard.propTypes = { getCurrentProfile: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, profile: PropTypes.object.isRequired, }; const mapStateToProps = (state) => ({ auth: state.auth, profile: state.profile, }); export default connect(mapStateToProps, { getCurrentProfile })(Dashboard);3. Starting On The Dashboard + Spinner
① NavbarにDashboardリンクを設置。
components/layout/Navbar.js//... const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => { const authLinks = ( <ul> + <li> + <Link to="/dashboard"> + <i className="fas fa-user" /> + <span className="hide-sm">Dashboard</span> + </Link> + </li> <li> //...② Landingに認証済みならDashboardに遷移するように設定。
components/layout/Landing.jsimport React from 'react'; import { Link, Redirect } from 'react-router-dom'; + import { connect } from 'react-redux'; + import PropTypes from 'prop-types'; + const Landing = ({ isAuthenticated }) => { + if (isAuthenticated) { + return <Redirect to="/dashboard" />; + } //... + Landing.propTypes = { + isAuthenticated: PropTypes.bool, + }; + const mapStateToProps = (state) => ({ + isAuthenticated: state.auth.isAuthenticated, + }); + export default connect(mapStateToProps)(Landing);③ スピナーを設置
spinner.gif
はインターネットから持ってくる。components/layout/Spinner.jsimport React, { Fragment } from 'react'; import spinner from `./spinner.gif`; export default () => ( <Fragment> <img src={spinner} style={{ width: '200px', margin: 'auto', display: 'block' }} alt='Loading...' /> </Fragment> );④ ログアウトした時に、前回ログインしたユーザーの情報がStateに残ってる原因を解消。
->
CLEAR_PROFILE
をauthアクションとprofileリデューサに設置。actions/types.jsexport const GET_PROFILE = 'GET_PROFILE'; export const PROFILE_ERROR = 'PROFILE_ERROR'; + export const CLEAR_PROFILE = 'CLEAR_PROFILE';actions/auth.js//... import { REGISTER_SUCCESS, REGISTER_FAIL, USER_LOADED, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT, + CLEAR_PROFILE, } from './types'; import setAuthToken from '../utils/setAuthToken'; //... // Logout / Clear Profile export const logout = () => (dispatch) => { + dispatch({ type: CLEAR_PROFILE }); dispatch({ type: LOGOUT }); };reducers/profile.js+ import { GET_PROFILE, PROFILE_ERROR, CLEAR_PROFILE } from '../actions/types'; //... case PROFILE_ERROR: return { ...state, error: payload, loading: false, profile: null, }; + case CLEAR_PROFILE: + return { + ...state, + profile: null, }; default: //...⑤ Spinnerコンポ設置と、Profile設定されてない場合の設定。
components/dashboard/Dashboard.jsimport React, { Fragment, useEffect } from 'react'; + import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; + import Spinner from '../layout/Spinner'; import { getCurrentProfile } from '../../actions/profile'; const Dashboard = ({ getCurrentProfile, + auth: { user }, + profile: { profile, loading }, }) => { useEffect(() => { getCurrentProfile(); }, []); // 以下すべて追加。 return loading && profile === null ? ( <Spinner /> ) : ( <Fragment> <h1 className="large">Dashboard</h1> <p className="lead"></p> <i className="fas fa-user"> Welcome {user && user.name}</i> {profile !== null ? ( <Fragment>has</Fragment> ) : ( <Fragment> <p>You have not yet set up a profile, please add some info</p> <Link to="/create-profile" className="btn btn-primary my-1"> Create Profile </Link> </Fragment> )} </Fragment> ); }; //...4. CreateProfile Component
① profile-formsフォルダ生成 -> profile-formsファイル生成
~~~ 詳細説明 ~~~
(1) rafcp -> 'Enter'
(2) HooksであるuseStateを設置
Hooks(Classコンポーネントと関数(const xx)コンポーネントの違い)とは
今までは...
・クラスはstateを保持できるが、関数はそれができない。
・クラスはライフサイクル(componentDidMount, componentDidUpdate)を持つが、関数はそれを持たない。
・クラスはPureComponentを継承できるが、関数は都度レンダリングされる。
が、しかし!!! これらを解消するのが、Hooooks!!
(Hooksのルール - 名前が「use」で始まらなければならない)
【書き方】 const [state名, set更新関数 ] = useState;(3) プロパティをformDataに格納
(4) html_themeをコピペ ->Fragment
で囲む ->class
をclassName
に変更
(5) ソーシャルメディアをトグル設定 'toggleSocialInputs'
(6)イベントハンドラーonChangeを定義(フォームの選択や入力欄に変更があった場合に指定の処理を行わせる)
-> HooksのuseStateを更新するためのset更新関数
を設定。(今回はsetFormData
)
->inputタグ
に、value={プロパティ名}
とonChange={(e) => onChange(e)}
を追加components/profile-forms/CreateProifile.js(2)import React, { Fragment, useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; const CreateProfile = (props) => { (2) const [formData, setFormData] = useState({ status: '', skills: '', bio: '', website: '', youtube: '', twitter: '', facebook: '', linkedin: '', }); (5) const [displaySocialInputs, toggleSocialInputs] = useState(false); (3) const { status, skills, bio, website, youtube, twitter, facebook, linkedin } = formData; const onChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }); return ( <Fragment> <h1 className="large text-primary">Create Your Profile</h1> <p className="lead"> <i className="fas fa-user"></i> Let's get some information to make your profile stand out </p> <small>* = required field</small> <form className="form"> <div className="form-group"> <select name="status" value={status} onChange={(e) => onChange(e)}> <option value="0">* Select Professional Status</option> <option value="Developer">Developer</option> <option value="Junior Developer">Junior Developer</option> <option value="Senior Developer">Senior Developer</option> <option value="Manager">Manager</option> <option value="Student or Learning">Student or Learning</option> <option value="Instructor">Instructor or Teacher</option> <option value="Intern">Intern</option> <option value="Other">Other</option> </select> <small className="form-text"> Give us an idea of where you are at in your career </small> </div> <div className="form-group"> <input type="text" placeholder="Website" name="website" value={website} onChange={(e) => onChange(e)} /> <small className="form-text"> Could be your own or a company website </small> </div> <div className="form-group"> <input type="text" placeholder="* Skills" name="skills" value={skills} onChange={(e) => onChange(e)} /> <small className="form-text"> Please use comma separated values (eg. HTML,CSS,JavaScript,PHP) </small> </div> <div className="form-group"> <textarea placeholder="A short bio of yourself" name="bio" value={bio} onChange={(e) => onChange(e)} ></textarea> <small className="form-text">Tell us a little about yourself</small> </div> <div className="my-2"> <button (5) onClick={() => toggleSocialInputs(!displaySocialInputs)} type="button" className="btn btn-light" > Add Social Network Links </button> <span>Optional</span> </div> (5) {displaySocialInputs && ( <Fragment> <div className="form-group social-input"> <i className="fab fa-twitter fa-2x"></i> <input type="text" placeholder="Twitter URL" name="twitter" value={twitter} onChange={(e) => onChange(e)} /> </div> <div className="form-group social-input"> <i className="fab fa-facebook fa-2x"></i> <input type="text" placeholder="Facebook URL" name="facebook" value={facebook} onChange={(e) => onChange(e)} /> </div> <div className="form-group social-input"> <i className="fab fa-youtube fa-2x"></i> <input type="text" placeholder="YouTube URL" name="youtube" value={youtube} onChange={(e) => onChange(e)} /> </div> <div className="form-group social-input"> <i className="fab fa-linkedin fa-2x"></i> <input type="text" placeholder="Linkedin URL" name="linkedin" value={linkedin} onChange={(e) => onChange(e)} /> </div> </Fragment> (5) )} <input type="submit" className="btn btn-primary my-1" /> <a className="btn btn-light my-1" href="dashboard.html"> Go Back </a> </form> </Fragment> ); }; CreateProfile.propTypes = {}; export default CreateProfile;② App.jsに
CreateProfile
ルートを設置src/App.js//... import Dashboard from './components/dashboard/Dashboard'; + import CreateProfile from './components/profile-forms/CreateProfile'; import PrivateRoute from './components/routing/PrivateRoute'; //... <PrivateRoute exact path="/dashboard" component={Dashboard} /> + <PrivateRoute exact path="/create-profile" component={CreateProfile} /> //...5. Create Profile Action
① Create or update profile
(1): withRouterとセットで使用。次の②参照。actions/profile.js//... // Create or update profile export const createProfile = (formData, history, edit = false) => async ( dispatch ) => { try { const config = { headers: { 'Content-Type': 'application/json', }, }; const res = await axios.post('/api/profile', formData, config); dispatch({ type: GET_PROFILE, payload: res.data, }); dispatch(setAlert(edit ? 'Profile Updated' : 'Profile Created'), 'sucess'); if (!edit) { (1) history.push('/dashboard'); } } catch (err) { const errors = err.response.data.errors; if (errors) { errors.forEach((error) => dispatch(setAlert(error.msg, 'danger'))); } dispatch({ type: PROFILE_ERROR, payload: { msg: err.response.statusText, status: err.response.status }, }); } };② ProfileコンポーネントにcreateProfileアクションを設置。
(1) react-routerのページ遷移をhandleで行う時にはwithRouterを使う
・react-routerでページ遷移の基本はLinkだが、onClickなどのhandleでは使えない
=> handleで呼び出すには、withRouter(XxxYyyy)とthis.props.history.push(/zzzz)を使う。components/profile-forms/CreateProfile.jsimport React, { Fragment, useState } from 'react'; (1)+ import { Link, withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; + import { createProfile } from '../../actions/profile'; (1)+ const CreateProfile = ({ createProfile, history }) => { const [formData, setFormData] = useState({ //... const onChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }); + const onSubmit = (e) => { + e.preventDefault(); (1)+ createProfile(formData, history); + }; //... <small>* = required field</small> + <form className="form" onSubmit={onSubmit}> <div className="form-group"> <select name="status" value={status} onChange={(e) => onChange(e)}> //... CreateProfile.propTypes = { + createProfile: PropTypes.func.isRequired, }; (1)+ export default connect(null, { createProfile })(withRouter(CreateProfile));6. Edit Profile
① Editボタン設置。
rafe -> 'Enter'
components/dashboard/DashboardActions.js//以下全て追加 import React from 'react'; import { Link } from 'react-router-dom'; const DashboardActions = () => { return ( <div className="dash-buttons"> <Link to="/edit-profile" className="btn btn-light"> <i className="fas fa-user-circle text-primary"></i> Edit Profile </Link> </div> ); }; export default DashboardActions;components/dashboard/Dashboard.js//... + import DashboardActions from './DashboardActions'; import { getCurrentProfile } from '../../actions/profile'; //... <Fragment> <h1 className="large">Dashboard</h1> <p className="lead"></p> <i className="fas fa-user"> Welcome {user && user.name}</i> {profile !== null ? ( <Fragment> + <DashboardActions /> </Fragment> ) : ( <Fragment> <p>You have not yet set up a profile, please add some info</p> //...② Edit Profileコンポーネント
(1) CreateProfile.jsと同じようなコードなのでコピペ
(2)useEffect
はクラスのライフサイクルcomponentDidMount, componentDidUpdateとcomponentWillUnmountの3つと同様な処理を行うことができるHooks。useEffectを利用することでコンポーネントをレンダリングする際に外部のサーバからAPIを経由してデータを取得したり、コンポーネントが更新する度に別の処理を実行するということが可能。
(参考URL) https://reffect.co.jp/react/react-useeffect-understanding#React_useEffect
(3) useEffectの第二引数[配列]
・[state変数]配列にstate変数を追加した場合 -> ある特定のstate変数の更新があった時だけuseEffectを実行する。=>「useEffectがstate変数に依存している状態」
・にした場合 -> 最初の一回のuseEffectは実行されるが、それ以降はuseEffectが更新されない。*しかし、空配列を渡すとバグを起こしやすい
(4) actions/profile.jsのcreateProfile変数にデフォルトだとedit = false
を格納しているので、EditProfileの時は->true
にしてやる。components/profile-forms/EditProfile.js(2)+ import React, { Fragment, useState, useEffect } from 'react'; import { Link, withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; + import { createProfile, getCurrentProfile } from '../../actions/profile'; + const EditProfile = ({ + profile: { profile, loading }, createProfile, + getCurrentProfile, history, }) => { const [formData, setFormData] = useState({ //... (2)//userEffect以下すべて追加 useEffect(() => { if (!profile) getCurrentProfile(); if (!loading && profile) { const profileData = { ...initialState }; for (const key in profile) { if (key in profileData) profileData[key] = profile[key]; } for (const key in profile.social) { if (key in profileData) profileData[key] = profile.social[key]; } if (Array.isArray(profileData.skills)) { profileData.skills = profileData.skills.join(', '); } setFormData(profileData); } (3) }, [loading, getCurrentProfile, profile]); //... const onChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }); const onSubmit = (e) => { e.preventDefault(); (4)+ createProfile(formData, history, true); }; //... + EditProfile.propTypes = { createProfile: PropTypes.func.isRequired, + getCurrentProfile: PropTypes.func.isRequired, + profile: PropTypes.object.isRequired, }; + const mapStateToProps = (state) => ({ + profile: state.profile, +}); + export default connect(mapStateToProps, { createProfile, getCurrentProfile })( withRouter(EditProfile) );③ App.jsにEditProfileのルート設定。
src/App.js//... import CreateProfile from './components/profile-forms/CreateProfile'; + import EditProfile from './components/profile-forms/EditProfile'; //... <PrivateRoute exact path="/create-profile" component={CreateProfile} /> + <PrivateRoute exact path="/edit-profile" component={EditProfile} /> </Switch> //...7. Delete Account
① TypeアクションにDELETE_ACCOUNT追加
actions/types.js//... + export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';② profileアクションに
deleteAccount
関数追加。-> 安易にアカウント削除できないように確認ウィンドウの設置。
actions/profile.js//... import { ACCOUNT_DELETED, CLEAR_PROFILE, GET_PROFILE, PROFILE_ERROR, ACCOUNT_DELETED, } from './types'; //... // Delete account & profile export const deleteAccount = () => async (dispatch) => { if (window.confirm('Are you sure? This can NOT be undone!')) { try { await axios.delete('api/profile'); dispatch({ type: CLEAR_PROFILE }); dispatch({ type: ACCOUNT_DELETED }); dispatch(setAlert('Your account has been permanently deleted')); } catch (err) { dispatch({ type: PROFILE_ERROR, payload: { msg: err.response.statusText, status: err.response.status }, }); } } };③ auth reducerにACCOUNT_DELETED追加。
reducers/auth.js//... LOGIN_FAIL, LOGOUT, + ACCOUNT_DELETED, } from '../actions/types'; //... case LOGIN_FAIL: case LOGOUT: + case ACCOUNT_DELETED: localStorage.removeItem('token'); return { ...state, token: null, isAuthenticated: false, loading: false, }; default: return state; } }④ アカウント削除ボタンを設置
components/dashboard/Dashboard.js//... import DashboardActions from './DashboardActions'; + import { getCurrentProfile, deleteAccount } from '../../actions/profile'; const Dashboard = ({ getCurrentProfile, + deleteAccount, auth: { user }, profile: { profile, loading }, }) => { useEffect(() => { getCurrentProfile(); }, [getCurrentProfile]); //... <Fragment> <DashboardActions /> + <div className="my-2"> + <button className="btn-danger" onClick={() => deleteAccount()}> + <i className="fas fa-user-minus"></i> Delete My Account + </button> + </div> </Fragment> //... Dashboard.propTypes = { getCurrentProfile: PropTypes.func.isRequired, + deleteAccount: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, profile: PropTypes.object.isRequired, }; const mapStateToProps = (state) => ({ auth: state.auth, profile: state.profile, }); + export default connect(mapStateToProps, { getCurrentProfile, deleteAccount })( Dashboard );⑤ ProfileAPIにユーザーアカウントを削除したら、そのユーザーのPostsも全て削除する設定
routes/api/profile.js//... const Profile = require('../../models/Profile'); const User = require('../../models/User'); + const Post = require('../../models/Post'); //... // @route DELETE api/profile // @desc Delete profile, user & posts // @access Private router.delete('/', auth, async (req, res) => { try { - // @todo - remove users posts + // Remove user posts + await Post.deleteMany({ user: req.user.id }); // Remove profile await Profile.findOneAndRemove({ user: req.user.id }); // Remove user await User.findOneAndRemove({ _id: req.user.id }); res.json({ msg: 'User deleted' }); } catch (err) { //...
- 投稿日:2020-09-20T02:56:00+09:00
Redux ExampleのTodoListをReact Native(expo)に置き換えて解説-AddTodo編
Reduxの公式チュートリアルにあるTodoListアプリのExampleですが、これをReact Nativeで置き換えてみようという記事です。
僕が参加したプロジェクトがReduxによるState管理を行っており、Reduxの概念などがイマイチ分からなかったため、勉強のためにReactNative + Reduxの簡単なアプリを作ってみようということで上記のチュートリアルにたどり着きました。ただこちらはReactでの実装でしたので、ReactNativeに置き換えてみました。さらに、こちらの記事を参考に僕なりにReduxについても解説を交えていきたいと思います。
ある程度ReactNative、Reduxについて理解している方が対象となります。
以下はReduxの概念系で非常にわかりやすかった記事です。
React + Redux の基本的な使い方
Redux入門【ダイジェスト版】10分で理解するReduxの基礎注:僕はゴリゴリの掛け出しエンジニアであり、自分の勉強としての投稿という面もあるので、もしミスや勘違い、ベストプラクティスではない、等がありましたら、コメントしていただけると幸いです。
Reduxとは
Reduxの概念を簡単に言うと、
store
reactのstateを保管する場所。
reducer
storeを書き換えるための関数。storeのstateを書き換える。
action
reducerに渡すjson。
紛らわしいが、action creatorが情報をjsonに加工する関数。actionがjson
connect
storeに保管されたstateはreact-reduxのconnect関数を経由して、componentに渡される。まずはexpoでアプリ作成
詳細な環境構築などは別記事を当たってください。僕は「expoによるReact Nativeのおさらい(最低限)」を参考にさせていただきました。
expo init todoExample cd todoExamplereduxをインストール
npm install --save redux react-reduxもう一度npm install(これしないとなぜか動かないです。かなりつまったので記事化しました。)
npm install参考:expo startをしてもQRコードが出てこない、Starting Webpackしてしまう、UnhandledPromiseRejectionWarningとなってしまう場合
Actions
Reduxでの機能開発ですが、actionsから開発していくことにします。
Redux ExampleのTodo Listをはじめからていねいに(3)の記事を参考にしています。
以下引用1.actionCreatorとreducerでフィルターの値をstore(state)に格納
2.フィルターの値によってviewを変更(手動でフィルターを操作して動作確認)
3.リンクをクリックしてフィルターを操作してviewを変更actionというのは、reduxの機能ではなく、純粋な関数です。(ファイルやディレクトリは適宜作成してください。)
以下でactionCreatorを定義します。src/actions.jslet nextTodoId = 0 export const addTodo = text => ({ type: 'ADD_TODO', id: nextTodoId++, text })上記の意味としては、「textという引数を取り、{type: "ADD_NAME", text: text}を返す関数」ということです。
Reducers
reducerも単なる関数で、上記のactionCreatorで作成されたactionと現在のstoreに保存されているstateを受け取り、新たなstateを返します。
src/reducers/todos.jsconst todoReducers = (state = [], action) => { switch(action.type){ case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text } ] default: return state } } export default todoReducersStore
Storeはアプリケーションで1つしかありません。stateを管理してくれます。
作り方としては、createStore関数でreducerを呼び出すことで作られます。src/store.jsimport { createStore } from 'redux' import todoReducers from '../reducers/todos' const store = createStore(todoReducers); export default store;一度動作確認
いったん、動作確認するためにsrc/components/Main.jsを作成しておきます。
src/components/Main.jsimport React, { Component } from 'react' import { View, Text, StyleSheet, } from 'react-native'; class Main extends Component { render(){ return ( <View style={styles.container}> <Text>Hello World!!</Text> </View> ); } } const styles = StyleSheet.create({ container: { marginTop: Platform.OS === "ios" ? 30 : 0, flex: 1, justifyContent: "space-between", flexDirection: "column", } }); export default Main;それでは、ここまでで、最初に紹介したaction -> reducer -> storeのデータの流れができました。ここで一度動作確認をしてみます。
src/store.jsimport { createStore } from 'redux' import todoReducers from './reducers/index' const store = createStore(todoReducers); export default store;上記で、expoを実行するとconsoleに
Array [ Object { "id": 0, "text": "Hello World!", }, ]このように表示されているはずです。
データの流れとしては、1.addTodo('Hello World!')で{"id": 0, "text": "Hello World!",}というオブジェクトを作成
2.store.dispatch関数にactionを渡すことで、store内にあるtodoReducerへ上記オブジェクトと現在のstateを渡す
3.store内でtodoReducerが新たなstateを返す
4.store.getState()でstore内のstateをgetという感じです。
ちなみに、画面には「Hello World!!」と表示されているだけです。Reducerファイルを分ける
今後、todosというreducer以外にも、reducerを作成していきますので、それぞれのreducerをまとめるファイルを作成します。
src/reducers/index.jsimport { combineReducers } from 'redux' import todoReducers from './todos' const todoAppReducers = combineReducers({ todoReducers }) export default todoAppReducersstore.jsの参照するreducerもしっかり変えておきましょう。
src/store.jsimport { createStore } from 'redux' import todoAppReducers from './reducers/index' //参照を変更 const store = createStore(todoAppReducers); //変更箇所 export default store;ContainerとComponentについて
ここから、Store内にあるstateをView側で表示させるという部分を実装していきます。
ここで、react + reduxにおけるViewの2つのコンポーネントについて解説します。
Presentational Components
- Reactでいうコンポーネントと全く同じ
- 単にpropsを受け取ってそれらを描画するコンポーネント
- Reduxの要素は特にない
Container Components
- storeとのstateからデータを受け取る
- stateのデータの変更もここで行う
Todoリストを表示するためのコンポーネント(Presentational Components)を作成
src/components/TodoList.jsimport React, { Component } from 'react' import { Text, FlatList, Button, View, StyleSheet } from 'react-native' class TodoList extends Component { render() { return( <View> <FlatList data={this.props.todos} renderItem={({item}) => <View style={style.todoList}> <Text> {item.text} </Text> </View> } keyExtractor={item => item.id.toString()} /> </View> ) } } const style = StyleSheet.create({ todoList: { marginBottom: 10, flexDirection: "row" } }) export default TodoList上記のdataの中の「this.props.todos」というのがこのあとすぐ実装するContainer Componentsから送られてくるtodoの一覧です。
ちなみに、FlatListの中のkeyExtractorはstring型でなければいけないため、toStirngでstring型へ変更しています。TodoListコンポーネントをReduxとつなげるためのコンポーネント(Container Components)の作成
src/containers/VisibleTodoList.jsimport { connect } from 'react-redux' import TodoList from '../components/TodoList' const mapStateToProps = (state) => { return { todos: state.todos } } const VisibleTodoList = connect( mapStateToProps )(TodoList) export default VisibleTodoListこのファイルでは、TodoListコンポーネントをconnectしています。
mapStateToPropsは、Store.getState()のような役割をして、ComponentのpropsにStateの中身を詰め込んでくれます。先ほど、src/components/TodoList.jsでthis.props.todosとするとtodoの一覧が取れると書きましたが、ここでmapStateToPropsを定義しているからです。
例えば、src/containers/VisibleTodoList.jsconst mapStateToProps = (state) => { return { todoList: state.todos } }のような形で渡せば、src/components/TodoList.jsではthis.props.todoListのような形で取り出します。
アプリに表示
ようやく、storeにtodoを追加し、Viewで表示するという一連の流れができましたので、アプリにtodoリストを表示してみます。
src/components/Main.jsimport VisibleTodoList from '../containers/VisibleTodoList' //追加 class Main extends Component { render(){ return ( <View style={styles.container}> <VisibleTodoList /> //変更 </View> ); } }Todoを追加するフォームはまだ作成していないので、直接追加していきます。
App.jsstore.dispatch(addTodo('Hello React!')) //追加 store.dispatch(addTodo('Hello Redux!')) //追加こんな感じになっているかと思います!
AddTodoのフォームを作成
次はフォームからTodoを作成できるようにAddTodoフォームを作成していきます。
先ほど、コンポーネントには2種類あると書きましたが、このAddTodoコンポーネントは、どちらでもありません。公式ドキュメントでは、Other Componentsという風になっています。
ディレクトリについては、今回はcontainers配下に作成します。src/containers/AddTodo.jsimport React, { Component } from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions/index' import { View, TextInput, StyleSheet, Button } from 'react-native'; class AddTodo extends Component { constructor(props) { super(props) this.state = { text: "", } } _addTodo () { this.props.dispatch(addTodo(this.state.text)) this.setState({ text: "" , }) } render() { return ( <View> <TextInput type="todoName" style={style.input} value={this.state.text} onChangeText={text => this.setState({text})} /> <Button title='追加' onPress={() => this._addTodo()} /> </View> ) } } const style = StyleSheet.create({ input: { height: 40, borderColor: 'gray', borderWidth: 1 } }) AddTodo = connect()(AddTodo) export default AddTodoButtonをクリックすると、_addTodo関数が呼ばれます。_addTodo関数では、
this.props.dispatch(addTodo(this.state.text))
でinputに入っているテキストをstore内のtodo一覧に追加します。src/components/Main.jsimport AddTodo from '../containers/AddTodo' //追加 class Main extends Component { render(){ return ( <View style={styles.container}> <AddTodo /> //追加 <VisibleTodoList /> </View> ); } }最後にMain.jsにAddTodoコンポーネントを追加すればAddTodo機能は完成です!!
お手元のシミュレーターでお試しください。
ここまでのソースコードはGitHubに上げていますのでご参考ください。次回はTodoの完了・未完了を切り替える「Toggle Todo」機能を実装していきます。
- 投稿日:2020-09-20T01:48:20+09:00
expo startをしてもQRコードが出てこない、Starting Webpackしてしまう、UnhandledPromiseRejectionWarningとなってしまう場合
タイトルの通りなのですが、expo startをした際に、いつもであればQRコードがでてきてアプリがスタートしてくれるのですが、QRコードが出てこない。さらになんか自動的にWebpackサーバーがスタートしてしまう。エラー文章を見るとUnhandledPromiseRejectionWarningというエラーが出ている。
1日中悩んでいたのですが、以下の記事のとある部分に重要なことが書かれていました。
expoによるReact Nativeのおさらい(最低限)なぜか再度npm installしないと使えません。。。
どうやら、expoではパッケージをnpm install でいれた場合、もう一度
npm installをしないといけないようです。理由は調べてみましたが、イマイチわかりませんでした。。
→もしわかる方いたらコメントしていただけると幸いです。これによってアプリの動作が非常におかしくなってしまったので、同じようなエラーで悩んでいる方の助けになれば幸いです。
また、ミスや僕の勘違いがありましたらご指摘いただけると大変ありがたいです!