20210502のReactに関する記事は14件です。

コードリーディングから学ぶ!(React.js公式チュートリアル編)

学習工程 先に完成プログラムのコードリーディング(code): 気づき プログラム写経(気づき,疑問点): 理解 プログラム改変(code, 理解): new_code の順で行います。 コードリーディング(以下、CR)では、コードを読んで不明な点を 「疑問点」として。 また、普段実装している時に自分が困りやすいところを実装している箇所があれば「気づき」としてメモ(結果返答配列に追加)してその結果をCR の結果として返します。 プログラミング写経では、CRで得た「疑問点」や「気づき」を写経しながら解決していきます。詳しくは、次稿で説明したいと思います! プログラム改変では、プログラミング写経で得た「理解」から現在のプログラムを改変(改造)していきます。詳しくは、次次稿で説明したいと思います! 本稿では、「1.コードリーディング」を行います! 前後関係 背景 React.js(以下、React)を使ったアプリを作ってみたく、プログラミング勉強兼ねてReact公式ホームページに掲載されているチュートリアルを敢えてコードリーディング(以下、CR)しました。 Reactチュートリアルを選んだ理由 React公式チュートリアルは、順を追ってアプリを作成しつつ進めると、学びを得ることができます。 しかし、私は順を追って学習するのが苦手で、先に完成品を眺めたり分析をした後解説を読んだ方が、頭への入りが幾分マシなのでこの方法をとっています。 ## 1. コードリーディング 取り組み方 ‘ソースコードを読むための技術’を参考に取り組み方を考える。 対象プログラムを動かして機能の洗い出し 略語の調査 データ構造を考える クラス・関数の関係を把握する(クラス図・コールグラフ作成) 終了条件(気づき>0 && 疑問点>0)でなければ、1からやり直し。 CR後の成果物 気づき(=既にある疑問に対しての答え)、 疑問点(=CRしたことで発生した疑問)、 改善点(=既存の知識での改良) なぜこの取り組み方法なのか 私は、 製品=要求機能+クラス・関数+データ が成り立つと考えています。 ですので、CRは実際に要求仕様を実装に落とし込むまでの手順の逆をすればいいと考えています。 例)実装まで 1. 実装機能を考える 2. コードの大まかな流れをかく 3. クラス図を作成する(クラスはプログラム全体の地図) 4. DFDを作成する(データについて考える) 5. 関数実装・単体テスト 6. 関数統合して実装・結合テスト 7. 完成、動かして受け入れテスト(機能を確かめている) これを逆から読むと今回の取り組み方に近い内容になっています。 想定読み方 完全初心者〜初心者の方 想定読者です。 プログラミング教室に学びに来た同期のまとめ資料を見る感覚でご覧ください。 内容を学ぶというより、CRを行う流れを「こういうやり方でやる奴もいるのかー」程度に参考にしていただけると幸いです。 もし、筆者のやり方を気に入っていただけましたら、一緒に順をおってCRやっていただけると、我非常喜!(本当は、「我很高兴(とっても嬉しい)」です笑) 初心者〜中級者の方 「あ、未経験の子ってこういう考え方するのかー。」と、暖かい目で厳しくしていただけると幸いです。 中級者〜上級者の方 ご指導、ご鞭撻のほどよろしくお願いいたします…! CR 対象プログラム See the Pen MWJMjrJ by kinoko dake (@kinoko_dake) on CodePen. 1. 対象プログラムを動かして機能の洗い出し 機能 表示に関わる機能 マス目をクリックしたら、XやOが表示される 「Next player:X」と、現在の手番の状態を表示 「1.Go to game# 2」と、履歴ボタンをクリックすると、それに紐付いている履歴にジャンプする。 ###### 見た目でわからない機能 ゲーム勝敗判定 2. 略語の調査 特になし! React公式の初心者向けラーニグコードなため、難しい略語がないのかも。 3. データ構造 // Game/constructor this.state = { history:[ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; Gameクラスで全てのコンポーネントデータと状態を管理している。 ゆえに、この箇所が対象プログラムの全てのデータである。 ハッシュにhistory(履歴)、st epNumber(手数)、xIsNext(どちらの手番か)を保持している。 historyは、盤上データを手数順に配列として管理している。 history:{squares:[Array(9)]} 図1 図2 図1の状態の実際のhistoryです。 [疑問点]historyは多次元配列ではダメなのか。 4.クラス・関数の関係を把握する クラス図は、プログラムの全体像を俯瞰しているイメージ。 関数のコールグラフ(的なの)はプログラムの部品を詳しく見るイメージ。 クラス図 ー 各クラスごとのコールグラフ Gameクラス内の関数同士関係 handleClickは、Boardがクリックされた時に呼び出されるイベントハンドラ。 [気になったコード] // handleClick関数 // 気になったコード 後で実装しながら確認 //疑問1 // current.squares[0]ではダメ? const squares = current.squares.slice();// 59行目 //疑問2 // history.concat!? pushじゃだめ? this.setState({ history: history.concat({ squares: squares }), }) jumpToは、履歴ボタンを押したときにジャンプする履歴に状態を更新します。 calculateWinnerは、現在の盤状態から勝敗を計算します。 // 気になったコード //疑問3 // な、なんぞ!? 頭が、、頭が痛い... 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] ]; //疑問4 // この関数内、大学数学講義室の匂いがする.. if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } // 気づき1 //三項演算子のインデント const desc = move ? 'Go to move #' + move : 'Go to game start'; Boardクラス内の関数同士関係 renderSquareは、外部Square関数を呼び出し9x9の盤のDOMオブジェクトを返す。 BoardクラスのrenderSquare関数は、外部実行ファイルのSquare関数を呼び出しています。 気づき、疑問点まとめ 気づき1:三項演算子のインデント 疑問点1:current.squares.slice() 疑問点2: history.concat 疑問点3:calculateWinner関数内ののマジカル多次元配列 疑問点4:calculateWinner関数内のマジカル条件 終了 CRはこれにておしまいです! 私は、CRにあまり時間をかけずに引っかかった箇所だけをメモしておき、 のちの写経にて問題解決をする方法をとっています。 実際、このチュートリアルはコード量も少なく比較的わかりやすい関数呼び出しを行なっているため、解析ツールも必要ありませんでした。 10分ほどで終わったと思います。 (しかし、この初投稿記事を書くのにほぼ半日かかってしまった…。 記事書いてる人って、神様なんだろうかガンジーなんだろうか。。) 次回は、「プログラム写経」を行なっていきたいと思います! 参考記事 本稿で参考にさせていただいた記事を紹介させていただきます! 1. kazuo_reveさんの「新人の方によく展開している有益な情報」 https://qiita.com/kazuo_reve/items/d1a3f0ee48e24bba38f1 2. ソースコードを読むための技術 https://i.loveruby.net/ja/misc/readingcode.html blog/codereading
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

useEffect内でsetTimeoutを使用する時の注意

TL;DR useEffect内でsetTimeoutを使用するときは、ちゃんとクリーンアップ処理を書きましょう。 useEffect(() => { let timeoutId = setTimeout(() => { // 5秒経過したら閉じる setIsOpen(false) }, 5000) // こういう処理を書く + return () => { + clearTimeout(timeoutId) + } }, []) クリーンアップ処理がないと起こりうるつらさ コンポーネント状態が更新され、かつその状態がsetTimeoutの設定に依存している場合、意図しない動きをするケースがあるからです。 例えば数秒待機して自動で閉じるモーダルを実装するとします。 この場合、コンポーネントの実装としては以下のようになるかと思います。 const Modal = () => { const [isOpen, setIsOpen] = useState(false) useEffect(() => { setTimeout(() => { // 5秒経過したら閉じる setIsOpen(false) }, 5000) }, []) return ( <div> <p>モーダル</p> </div> ) } ただコンポーネントの状態がこれのみならば、これで問題なく動いてくれると思います。 ここで、状態が更新されるモーダルを考えましょう。 const modalReducer = ( state: ModalKind, action: ModalKindAction ) => { switch (action.type) { case "info": return { type: action.type, message: "通知です" } case "success": return { type: action.type, message: "成功です" } case "error": return { type: action.type, message: "エラーです" } default: return state } } const VariousModal = () => { const [isOpen, setIsOpen] = useState(false) const [modalKind, dispatchModalKind] = useReducer(modalReducer, { type: "info", message: "通知です", }) useEffect(() => { setTimeout(() => { // 5秒経過したら閉じる setIsOpen(false) }, 5000) }, []) return ( <div> <h2>{modalKind.type}</h2> <p>{modalKind.message}</p> </div> ) } info、success、error の三つの状態を、利用シーンに応じて使い分けるような共通モーダルです。 この時点でも特に問題はないです。 では、エラーの場合のみ、モーダルを閉じない仕様を考えます。 const VariousModal = () => { const [isOpen, setIsOpen] = useState(false) const [modalKind, dispatchModalKind] = useReducer(modalReducer, { type: "info", message: "通知です", }) useEffect(() => { if (modalKind.type !== "error") { setTimeout(() => { // 5秒経過したら閉じる setIsOpen(false) }, 5000) } }, []) return ( <div> <h2>{modalKind.type}</h2> <p>{modalKind.message}</p> </div> ) } 例えばデータフェッチなどでinfo状態のモーダルから、データが取得され次第 成功/失敗 のモーダルに更新表示したい場合などに、5秒待たずしてerror状態になったとします。 エラー時にはモーダルが閉じてほしくないですが、上記のコードだとエラー状態にもかかわらず自動でモーダルが閉じる現象が発生します。 なぜか? setTimeoutのクリーンアップ処理を書いていないため、info状態で設定したsetTimeoutが生きてるからです。 以下のように書くと、コンポーネント更新時に、既存のsetTimeoutを削除してくれます。 const VariousModal = () => { const [isOpen, setIsOpen] = useState(false) const [modalKind, dispatchModalKind] = useReducer(modalReducer, { type: "info", message: "通知です", }) useEffect(() => { let timeoutId = null if (modalKind.type !== "error") { timeoutId = setTimeout(() => { // 5秒経過したら閉じる setIsOpen(false) }, 5000) } return () => { // クリーンアップ処理 if (timeoutId) { clearTimeout(timeoutId) } } }, []) return ( <div> <h2>{modalKind.type}</h2> <p>{modalKind.message}</p> </div> ) } まとめ 今回の例では種類に応じて切り替わるモーダルを例にしましたが、このケースはモーダルの種類毎にコンポーネントを別に作成することで回避することもできます。 あくまでsetTimeoutをuseEffectで使用する際に、クリーンアップ処理を書かないことで直感的でない動きをすることもあるということの一例として見てもらえれば幸いです。 特にコンポーネントの状態にsetTimeoutの実行が依存する場合などは、作法的にクリーンアップ処理を書いてしまっても良いのではないかと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hook 一覧①

Reactのフックについて説明していきます。 Hookとは接続という意味です。 ①useState 更新の時に使う。 例えば、ボタンを押す度に数字が+1になるときなどは、 ボタンを押すたびに更新をしなきゃなりません。 このような更新をする時に使います。 const [state, setState] = useState() ②useEffect 第二引数の依存している値が変更される度に第一引数が実行される。 この他にも使い方はありますが今回は理解できるように細かい使い方は割愛しています。 const hookFunction = () => { const [count, setCount] = useState(0) useEffect( () => { console.log(count); }, [count] ); return( <div> <p>{count}</p> <button onClick={() => setCount(count + 1)}>プラス1する</button> </div> ); } ③useContext これを使うにはまずは下記のように入力する必要があります。 export const UserCount = React.createContext() これを親コンポーネントで使用したら、valueをいれることができて、そのvalueを子のコンポーネントでも使えるようになります。 const value = useContext(MyContext); 下記の記事が非常にわかりやすいです ④useReducer useStateの代替品です。 useStateを使う時はシンプルなロジックで更新をしたいときに使います。 useReducerはロジックが複数あり複雑だったり、複数のロジックを分かり易くしたいときに使います。ボタンを押したら初期値に戻るなどのアクションをするときも便利です。 公式サイトがわかりやすいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[簡単]モダン構成なSPAで作るチュートリアル③(Reactで一覧テーブル編)~map()とコンポーネント分割~

SPAで作るタスク管理アプリのチュートリアル - 一覧テーブル編 今回は第③弾でReactで一覧テーブル作成する部分をやっていくで ①環境構築(Docker/Laravel/React.js/Material-UI) ②React側でルーティング設定 ③Reactで一覧テーブル作成 ④seederで作ったDBのデータをReactに渡して一覧に表示 ⑤新規登録機能 ⑥編集・削除機能 map()とコンポーネント分割の話が中心やで こんな感じで一覧表示するものを作っていくわな。 まず表示できるものをコピペしてから、 現場でよく使われる書き方として再利用しやすいコンポーネント分割や データの受け渡しを意識した記述にリファクタリングしながら知識習得していくな。 React.jsで一覧テーブルを表示する まず表示させてから追って解説してく感じにするな。 今あるjs/Home.jsを下記のようにそっくり入れ替えてOKやで。 Home.js import React from 'react'; import { Button, Card } from '@material-ui/core'; import { makeStyles, createStyles } from '@material-ui/core/styles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import purple from '@material-ui/core/colors/purple'; //スタイルの定義 const useStyles = makeStyles((theme) => createStyles({ card: { margin: theme.spacing(5), padding: theme.spacing(3), }, table: { minWidth: 650, }, tableHead: { backgroundColor: purple['A100'], }, })); //ヘッダーのコンテンツ用の配列定義 const headerList = ['名前', 'タスク内容', '編集', '完了']; function Home() { //定義したスタイルを利用するための設定 const classes = useStyles(); return ( <div className="container"> <div className="row justify-content-center"> <div className="col-md-10"> <div className="card"> <h1>タスク管理</h1> <Card className={classes.card}> {/* テーブル部分の定義 */} <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> {/* ヘッダー部分 */} <TableHead className={classes.tableHead}> <TableRow> {headerList.map((item, index) => ( <TableCell align="center" key={index}>{item}</TableCell> ))} </TableRow> </TableHead> {/* ボディ部分 */} <TableBody> <TableRow> <TableCell align="center">モーリー</TableCell> <TableCell align="center">肩トレ</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> <TableRow> <TableCell align="center">ドンキーコング</TableCell> <TableCell align="center">バナナ補給</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> </TableBody> </Table> </TableContainer> </Card> </div> </div> </div> </div> ); } export default Home; ビルド $ make npm-dev これで一旦localhostにアクセスしたら表示はできるようになったと思うわ いきなり貼っつけてなんやんねんって感じやろうから上のコードについてこれから説明行くで makeStylesとcreateStylest Home.js import { makeStyles, createStyles } from '@material-ui/core/styles'; @material-uiを使うためにimportしてるね。 makeStylesとcreateStylesはReactで使えるcssをいい感じに当てるために使うもんやと思っといていいと思う。 importする → useStylesを定義 → コンポーネント内でuseStyles()する → classNameに指定する って感じで使えるで。 cardって定義したやつに注目すると下記みたいになってるで //cardに関係ある部分だけ抜粋 import { makeStyles, createStyles } from '@material-ui/core/styles'; //import ↓ const useStyles = makeStyles((theme) => createStyles({ card: { //useStylesとして定義 margin: theme.spacing(5), padding: theme.spacing(3), }, ↓ function Home() { const classes = useStyles(); //Homeコンポーネント内でclassesとして定義 ↓ <Card className={classes.card}> //classNameに指定 理屈が知りたい人はMaterial-UIの公式ドキュメントをしっかり読めばいいと思うけど、 使っていくうちになれるから、このへんはなんとなくこんなふうに定義して呼び出すんやなーと思っといたら大丈夫やと思うわ。 適当に別のスタイル定義してコンポーネントに当てたりしてUI変えてみると理解深まると思うで TableのStyleについて これもmaterial-UIのコンポーネントを呼び出して使ってる感じ屋根 import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; これも公式ドキュメントにそれぞれのコンポーネントについてわかりやすく解説してあるから、 コンポーネントごとの詳細な仕様とかはそっち見てもらったらいいと思うわ https://material-ui.com/ja/components/tables/ Home.js ---------------------------- <Table className={classes.table} aria-label="simple table"> {/* ヘッダー部分 */} <TableHead className={classes.tableHead}> <TableRow> {headerList.map((item, index) => ( <TableCell align="center" key={index}>{item}</TableCell> ))} </TableRow> </TableHead> {/* ボディ部分 */} <TableBody> <TableRow> <TableCell align="center">モーリー</TableCell> <TableCell align="center">肩トレ</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> <TableRow> <TableCell align="center">ドンキーコング</TableCell> <TableCell align="center">バナナ補給</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> </TableBody> </Table> -------------- 構成を抜き出すと下記の構成になってるね。 Home.js <Table className={classes.table} aria-label="simple table"> <TableHead> </TableHead> <TableBody> </TableBody> </Table> その他style Home.js import Paper from '@material-ui/core/Paper'; import purple from '@material-ui/core/colors/purple'; 色とかデザイン関連のものも呼び出して使える感じやね。 ヘッダーに使用しているmapメソッドについて理解する mapはReact使ってると理解必須やし頻繁に出てくるから理解しいくで ヘッダーに関係あるところだけ抜き出すと下記の様になるな Home.js //ヘッダーのコンテンツ用の配列定義 const headerList = ['名前', 'タスク内容', '編集', '完了']; ↓ {/* ヘッダー部分 */} <TableHead className={classes.tableHead}> <TableRow> {headerList.map((item, index) => ( <TableCell align="center" key={index}>{item}</TableCell> ))} </TableRow> </TableHead> 配列で渡ってくるコンテンツをmap()で回しながら、TableRowのなかで 存在する数だけをつくっているな mapで生成される結果については以下やな(上のコードと見比べてみてや) Home.js <TableRow> <TableCell align="center" key="0">名前</TableCell> <TableCell align="center" key="1">タスク内容</TableCell> <TableCell align="center" key="2">編集</TableCell> <TableCell align="center" key="3">完了</TableCell> </TableRow> map()で書くとコードがスッキリするし、数が不確定データが渡ってくることを想定すると よく使うからmap()が分からん人はしっかり理解しとくといいで。 その上で何やけどテーブルのボディ部分についても現状TableCellを手続き的に書いていってるねんけど、ここも、Laravelからデータを受け取る想定のカタチにするためにリファクタリングしていくな。 つぎの部分からHome.jsのコードを変えてていくから差分をわかりやすくするためにコミットしておくで $ git add . $ git commit -m "一覧テーブルの表示" テーブルのボディ部分をデータがバックエンドから渡ってくることを想定した定義にリファクタリング 現状のTableRowの中身は下記の通りやね Home.js <TableBody> <TableRow> <TableCell align="center">モーリー</TableCell> <TableCell align="center">肩トレ</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> <TableRow> <TableCell align="center">ドンキーコング</TableCell> <TableCell align="center">バナナ補給</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> </TableBody> TableBodyの中でTableRowが渡ってくる数だけ繰り返されることになるから、rowをまとめる変数としてrowsを定義して 画像で見るとこんなイメージ 丸した部分のTableCellの中身をオブジェクトにして変数rowsの要素の一つとして扱う感じやね。 じゃあrowsを定義するわ Home.js const headerList = ['名前', 'タスク内容', '編集', '完了']; //headrListの下あたりにrowsを定義する let rows = [ { name: "モーリー", content: "肩トレ", editBtn: <Button color="secondary" variant="contained">編集</Button>, deleteBtn: <Button color="primary" variant="contained">完了</Button>, },{ name: "ドンキーコング", content: "バナナ補給", editBtn: <Button color="secondary" variant="contained">編集</Button>, deleteBtn: <Button color="primary" variant="contained">完了</Button>, }, ]; ボタンは固定やし渡ってくるデータじゃないから切り出す必要あるのかって思うかもしれんけど、後々テーブル部分をコンポーネントとして切り出したときに、外から渡してあげるほうが再利用性が高いから今回は切り出すことにしてるで。 作った配列をTableBodyで展開(map)していくで 旧コードの部分を下記のように変更してや Home.js (旧コード) ----------------------------------------------- </TableHead> {/* ボディ部分 */} <TableBody> <TableRow> <TableCell align="center">モーリー</TableCell> <TableCell align="center">肩トレ</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> <TableRow> <TableCell align="center">ドンキーコング</TableCell> <TableCell align="center">バナナ補給</TableCell> <TableCell align="center"><Button color="secondary" variant="contained">編集</Button></TableCell> <TableCell align="center"><Button color="primary" variant="contained">完了</Button></TableCell> </TableRow> </TableBody> </Table> ↓ Home.js (リファクタリング後コード) ----------------------------------------------- </TableHead> {/* ボディ部分 */} <TableBody> {rows.map((row, index) => ( <TableRow key={index}> //mapで回すときはkeyが必要 <TableCell align="center">{row.name}</TableCell> <TableCell align="center">{row.content}</TableCell> <TableCell align="center">{row.editBtn}</TableCell> <TableCell align="center">{row.deleteBtn}</TableCell> </TableRow> ))} </TableBody> </Table> mapで使う形になったな。 ビルドする $ make npm-dev 表示確認できたな。 rowの中で更にmapする rowの中でも繰り返しが行われている箇所があるね。 それはTableRowの中で列を見たときにTableCellが繰り返されていることがわかると思う。 これはさっき定義したオブジェクトの部分なんやけど、このオブジェクトもmapしていくことにするな。 TableRowの中身を下記のように変更するで Home.js <TableBody> {rows.map((row, index) => ( <TableRow key={index}> {Object.keys(row).map(function(key, i) { return( <TableCell align="center" key={i}>{row[key]}</TableCell> ); })} </TableRow> ))} </TableBody> ちょっとわかりにくいかもしれへんけど rowに対してmapして{name, content, editBtn, deleteBtn}をkeyとして利用している感じやね。 それでもよく分からん人は下記の記事参照すればいいと思うわ map()使ってテーブルの中に展開する記述は現場でよく見るからここで理解しておくことを強くおすすめするね。 ビルドする $ make npm-dev リファクタリングしても表示できることが確認出来たらコミットしておこう $ git add . $ git commit -m "テーブル部分をmap利用するようにリファクタリング" コミットまで出来たらおさらいで、リファクタリング前のコードと見比べてみると復習になると思うで。 データが2つやから恩恵がわからんかもしれんけど、バックエンドからデータが大量に渡される想定で考えたらmapで書いてるほうが扱いやすいはずやわ。 でさらに自分のエディタで変更後のテーブルブブンのソースに着目してほしいねんけど、表示コンテンツが全部変数化されてて固定値が一つもないな。 こーゆー風に表示コンテンツを外からもらう形にすることでテーブル部分だけをコンポーネント化して切り出すことができるねん。 切り出すことで、アプリケーション内の他のページで一覧表を表示したいときに再利用できて 見た目的にも統一感のあるアプリーケーションが作れるようになるで テーブル部分をコンポーネントとして切り出す /conponents/MainTable.jsを作成する componentsディレクトリは現状空ディレクトリとして存在するはずやからそこに作っていくで 切り出すのはの中からで Home.jsから切り取って以下のように設定するで MainTable.js function MainTable() { return ( <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> {/* ヘッダー部分 */} <TableHead className={classes.tableHead}> <TableRow> {headerList.map((item, index) => ( <TableCell align="center" key={index}>{item}</TableCell> ))} </TableRow> </TableHead> {/* ボディ部分 */} <TableBody> {rows.map((row, index) => ( <TableRow key={index}> {Object.keys(row).map(function(key, i) { return( <TableCell align="center" key={i}>{row[key]}</TableCell> ); })} </TableRow> ))} </TableBody> </Table> </TableContainer> ); } export default MainTable; コンポーネントの中身が移動出来たらimport部分とスタイル部分をHome.jsから移してくるで。 HomeとMainTable両方で使ってるものはコピーしてHomeでは使わなくなったものは切り取って来る。 それぞれ↓みたいになるで MainTable.js import React from 'react'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import purple from '@material-ui/core/colors/purple'; import { makeStyles, createStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme) => createStyles({ card: { margin: theme.spacing(5), padding: theme.spacing(3), }, table: { minWidth: 650, }, tableHead: { backgroundColor: purple['A100'], }, })); function MainTable(props) { //定義したスタイルを利用するための設定 const classes = useStyles(); // --------以下は既に移してきているところ return ( <TableContainer component={Paper}> Home.jsに残った部分は以下のようになるで Home.js import React from 'react'; import { Button, Card } from '@material-ui/core'; import { makeStyles, createStyles } from '@material-ui/core/styles'; import MainTable from '../components/MainTable'; //スタイルの定義 const useStyles = makeStyles((theme) => createStyles({ card: { margin: theme.spacing(5), padding: theme.spacing(3), }, })); // ------------- 以下の部分は変更無し //ヘッダーのコンテンツ用の配列定義 const headerList = ['名前', 'タスク内容', '編集', '完了']; これでコンポーネントの切り出しは出来たで。 ただこれでは動かないから、Home.jsの中でMainTable.jsを使う設定をして MainTable.jsに変数を渡してあげる必要があるで Home.js import React from 'react'; import { Button, Card } from '@material-ui/core'; import { makeStyles, createStyles } from '@material-ui/core/styles'; import MainTable from '../components/MainTable'; // 追記する ------------------------------------               //テーブルを切り出した部分でMainTableコンポーネントを利用し変数を渡す <h1>タスク管理</h1> <Card className={classes.card}> {/* テーブル部分の定義 */} <MainTable headerList={headerList} rows={rows} /> //追記する </Card> </div> MainTable.js function MainTable(props) { //定義したスタイルを利用するための設定 const classes = useStyles(); //親コンポーネントからpropsで受け取る const {headerList, rows} = props; //追記する部分 return ( <TableContainer component={Paper}> Home.jsにMainTableコンポーネントを呼び出す記述を追加してを呼び出す。 その際にheaderList, rowsを渡して上げる必要がある MainTable.js側では親コンポーネント(Home.js)で渡した変数をpropsとして受け取るで。 この辺の知識が浅い人はReactそのものの基礎知識やから、React単体のチュートリアルとかで知識補っといてな。 ビルドする $ make npm-dev localhostにアクセスして、表示確認できたOKやで コミットしておこう $ git add . $ git commit -m "MainTableコンポーネントとして切り出す" 今回出てきたmap()やコンポーネント間のデータの受け渡しは、React案件では基礎知識になるからわからんかった人はReact単体の学習教材等で知識補ったらええと思うわ。 Material-UIの使い方もマスターしておきたいところやね。 じゃあ次はいよいよ今回の一覧表にLaravel側からデータを渡して表示する部分をやっていくで。 ほなLGTMよろしゅーやで。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TODO アプリで useReducer + useContext 入門

フロントエンド強化月間投稿記事 はじめに 「React Hooks と TypeScript で簡単 TODO アプリ」の続編です。 See the Pen TODO App by React & TypeScript by sprout2000 (@sprout2000_jp) on CodePen. 前回までの TODO のソースコード src/index.tsx import React, { useState } from 'react'; import ReactDOM from 'react-dom'; import './index.scss'; interface Todo { value: string; id: number; checked: boolean; removed: boolean; } type Filter = 'all' | 'checked' | 'unchecked' | 'removed'; const App: React.VFC = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const [filter, setFilter] = useState<Filter>('all'); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = ( e: React.FormEvent<HTMLFormElement | HTMLInputElement> ) => { e.preventDefault(); if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, removed: false, }; setTodos([newTodo, ...todos]); setText(''); }; const handleOnEdit = (id: number, value: string) => { const newTodos = todos.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); setTodos(newTodos); }; const handleOnCheck = (id: number, checked: boolean) => { const newTodos = todos.map((todo) => { if (todo.id === id) { todo.checked = !checked; } return todo; }); setTodos(newTodos); }; const handleOnRemove = (id: number, removed: boolean) => { const newTodos = todos.map((todo) => { if (todo.id === id) { todo.removed = !removed; } return todo; }); setTodos(newTodos); }; const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => { setFilter(e.target.value as Filter); }; const handleOnEmpty = () => { const newTodos = todos.filter((todo) => !todo.removed); setTodos(newTodos); }; const filteredTodos = todos.filter((todo) => { switch (filter) { case 'all': return !todo.removed; case 'checked': return !todo.removed && todo.checked; case 'unchecked': return !todo.removed && !todo.checked; case 'removed': return todo.removed; default: return todo; } }); return ( <div className="container"> <select className="select" defaultValue="all" onChange={handleOnFilter}> <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">現在のタスク</option> <option value="removed">削除済みのタスク</option> </select> {filter === 'removed' ? ( <button className="empty" onClick={handleOnEmpty}> ごみ箱を空にする </button> ) : ( <form className="form" onSubmit={handleOnSubmit}> <input className="text" type="text" disabled={filter === 'checked'} value={text} onChange={handleOnChange} /> <input className="button" type="submit" disabled={filter === 'checked'} value="追加" onSubmit={handleOnSubmit} /> </form> )} <ul> {filteredTodos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input className="text" type="text" disabled={todo.checked || todo.removed} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button className="button" onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? '復元' : '削除'} </button> </li> ); })} </ul> </div> ); }; ReactDOM.render(<App />, document.getElementById('root')); useReducer の導入 useReducer は useState の代替品で、複数の値にまたがる複雑な state ロジックがある場合や、前の state に基づいて次の state を決める必要がある場合に有用。 1. useReducer の構文 const [state, dispatch] = useReducer(reducer, 'ステートの初期値'); reducer はステートを更新するための関数 dispatch は reducer を実行するための呼び出し関数 2. dispatch による reducer メソッドの呼び出し 従前は setState(newState) を実行していたところを以下のようにステートを更新する。 dispatch(action); action は識別子 type プロパティと値のプロパティで構成されたオブジェクトで、そのアクションが何をするのかを示す。具体的には次のように利用される。 // count ステートを +1 する dispatch({ type: 'add', value: state.count + 1 }); /* * 以下と(ほぼ)同義 * setCount((count) => count + 1); */ 3. ステートの型を定義する この TODO アプリでは3つのステートが使われているので、それぞれの型を定義しておく。 src/index.tsx interface Todo { value: string; id: number; checked: boolean; removed: boolean; } type Filter = 'all' | 'checked' | 'unchecked' | 'removed'; interface State { text: string; todos: Todo[]; filter: Filter; } 4. ステートの初期値を設定する 上の型定義を利用して各ステートの初期値を設定する。 src/index.tsx interface State { text: string; todos: Todo[]; filter: Filter; } const initialState: State = { text: '', todos: [], filter: 'all', }; 5. action の要件を検討する 従前の TODO アプリでステートを更新しているコールバック関数は以下の7つ。 handleOnChange: (e: string) => void; handleOnSubmit: (e: Event) => void; handleOnEdit: (id: number, value: string) => void; handleOnCheck: (id: number, checked: boolean) => void; handleOnRemove: (id: number, removed: boolean) => void; handleOnFilter: (e: Event) => void; handleOnEmpty: () => void; これらを置き換えるような action を揃えてゆく。 handleOnChange と handleOnFilter src/index.tsx type Action = | { type: 'change'; value: string } | { type: 'filter'; value: Filter }; handleOnSubmit と handleOnEmpty を追加 src/index.tsx type Action = | { type: 'change'; value: string } | { type: 'filter'; value: Filter }; | { type: 'submit' } | { type: 'empty' }; handleOnEdit, handleOnCheck, handleOnRemove を追加 src/index.tsx type Action = | { type: 'change'; value: string } | { type: 'filter'; value: Filter }; | { type: 'submit' } | { type: 'empty' } | { type: 'edit'; id: number; value: string } | { type: 'check'; id: number; checked: boolean } | { type: 'remove'; id: number; removed: boolean }; 6. ステートを更新する reducer メソッドを作成 reducer メソッド の型 (state: State, action: Action) => newState: State State 型と Action 型を引数として受け取り、新しいステートを返す。 実装 src/index.tsx const reducer = (state: State, action: Action): State => {}; 上記7つのコールバック関数を Action の中へ落とし込んでゆく。 src/index.tsx const reducer = (state: State, action: Action): State => { switch (action.type) { case 'change': { return { ...state, text: action.value }; } case 'submit': { if (!state.text) return state; const newTodo: Todo = { value: state.text, id: new Date().getTime(), checked: false, removed: false, }; return { ...state, todos: [newTodo, ...state.todos], text: '' }; } case 'filter': return { ...state, filter: action.value }; case 'edit': { const newTodos = state.todos.map((todo) => { if (todo.id === action.id) { todo.value = action.value; } return todo; }); return { ...state, todos: newTodos }; } case 'check': { const newTodos = state.todos.map((todo) => { if (todo.id === action.id) { todo.checked = !action.checked; } return todo; }); return { ...state, todos: newTodos }; } case 'remove': { const newTodos = state.todos.map((todo) => { if (todo.id === action.id) { todo.removed = !action.removed; } return todo; }); return { ...state, todos: newTodos }; } case 'empty': { const newTodos = state.todos.filter((todo) => !todo.removed); return { ...state, todos: newTodos }; } default: return state; } }; 7. useState を useReducer へ置き換え useReducer のインポート src/index.tsx - import React, { useState } from 'react'; + import React, { useReducer } from 'react'; useState フックたちを削除し、 reducer メソッドと初期ステートによって state と dispatch を作成。 src/index.tsx const App: React.VFC = () => { const [state, dispatch] = useReducer(reducer, initialState); 8. 7つのコールバック関数の setHoge を dispatch へ置き換える handleOnChage src/index.tsx const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { dispatch({ type: 'change', value: e.target.value }); }; handleOnSubmit src/index.tsx const handleOnSubmit = ( e: React.FormEvent<HTMLFormElement | HTMLInputElement> ) => { e.preventDefault(); dispatch({ type: 'submit' }); }; handleOnEdit src/index.tsx const handleOnEdit = (id: number, value: string) => { dispatch({ type: 'edit', id, value }); }; handleOnCheck src/index.tsx const handleOnCheck = (id: number, checked: boolean) => { dispatch({ type: 'check', id, checked }); }; handleOnRemove src/index.tsx const handleOnRemove = (id: number, removed: boolean) => { dispatch({ type: 'remove', id, removed }); }; handleOnFilter src/index.tsx const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => { dispatch({ type: 'filter', value: e.target.value as Filter }); }; handleOnEmpty src/index.tsx const handleOnEmpty = () => { dispatch({ type: 'empty' }); }; 9. 関数内や JSX 内でステートを参照している部分を state.hoge へ修正 src/index.tsx const filteredTodos = state.todos.filter((todo) => { switch (state.filter) { case 'all': return !todo.removed; case 'checked': return !todo.removed && todo.checked; case 'unchecked': return !todo.removed && !todo.checked; case 'removed': return todo.removed; default: return todo; } }); return ( <div className="container"> <select className="select" defaultValue="all" onChange={handleOnFilter}> <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">現在のタスク</option> <option value="removed">削除済みのタスク</option> </select> {state.filter === 'removed' ? ( <button className="empty" onClick={handleOnEmpty}> ごみ箱を空にする </button> ) : ( <form className="form" onSubmit={handleOnSubmit}> <input className="text" type="text" disabled={state.filter === 'checked'} value={state.text} onChange={handleOnChange} /> <input className="button" type="submit" disabled={state.filter === 'checked'} value="追加" onSubmit={handleOnSubmit} /> </form> )} <ul> 10. 各部をコンポーネントに切り出して memo 化する コンポーネントを memo 化すると props に変化がない限り再計算されないため、パフォーマンスが向上する(こともある) dispatch を props として渡すことで各コンポーネントからのステート更新も可能 src/index.tsx - import React, { useReducer } from 'react'; + import React, { useReducer, memo, Dispatch } from 'react'; Selector コンポーネント src/index.tsx const Selector: React.VFC<{ dispatch: Dispatch<Action> }> = memo( ({ dispatch }) => { const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => { dispatch({ type: 'filter', value: e.target.value as Filter }); }; return ( <select className="select" defaultValue="all" onChange={handleOnFilter}> <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">現在のタスク</option> <option value="removed">削除済みのタスク</option> </select> ); } ); Selector.displayName = 'Selector'; EmptyButton コンポーネント src/index.tsx const EmptyButton: React.VFC<{ dispatch: Dispatch<Action> }> = memo( ({ dispatch }) => { const handleOnEmpty = () => { dispatch({ type: 'empty' }); }; return ( <button className="empty" onClick={handleOnEmpty}> ごみ箱を空にする </button> ); } ); EmptyButton.displayName = 'EmptyButton'; Form コンポーネント src/index.tsx const Form: React.VFC<{ state: State; dispatch: Dispatch<Action> }> = memo( ({ state, dispatch }) => { const handleOnSubmit = ( e: React.FormEvent<HTMLFormElement | HTMLInputElement> ) => { e.preventDefault(); dispatch({ type: 'submit' }); }; const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { dispatch({ type: 'change', value: e.target.value }); }; return ( <form className="form" onSubmit={handleOnSubmit}> <input className="text" type="text" disabled={state.filter === 'checked'} value={state.text} onChange={handleOnChange} /> <input className="button" type="submit" disabled={state.filter === 'checked'} value="追加" onSubmit={handleOnSubmit} /> </form> ); } ); Form.displayName = 'Form'; FilteredTodos コンポーネント src/index.tsx const FilteredTodos: React.VFC<{ state: State; dispatch: Dispatch<Action>; }> = memo(({ state, dispatch }) => { const handleOnEdit = (id: number, value: string) => { dispatch({ type: 'edit', id, value }); }; const handleOnCheck = (id: number, checked: boolean) => { dispatch({ type: 'check', id, checked }); }; const handleOnRemove = (id: number, removed: boolean) => { dispatch({ type: 'remove', id, removed }); }; const filteredTodos = state.todos.filter((todo) => { switch (state.filter) { case 'all': return !todo.removed; case 'checked': return !todo.removed && todo.checked; case 'unchecked': return !todo.removed && !todo.checked; case 'removed': return todo.removed; default: return todo; } }); return ( <ul> {filteredTodos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input className="text" type="text" disabled={todo.checked || todo.removed} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button className="button" onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? '復元' : '削除'} </button> </li> ); })} </ul> ); }); FilteredTodos.displayName = 'FilteredTodos'; 親コンポーネント src/index.tsx const App: React.VFC = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div className="container"> <Selector dispatch={dispatch} /> {state.filter === 'removed' ? ( <EmptyButton dispatch={dispatch} /> ) : ( <Form state={state} dispatch={dispatch} /> )} <FilteredTodos state={state} dispatch={dispatch} /> </div> ); }; useContext の導入 典型的な React アプリケーションでは、データは props を通して親から子、そして孫へと順々に渡されるが、useContext フック を利用することで、ツリーの各階層で明示的にプロパティを渡すことなく、コンポーネント間でこれらの値を共有できる。 画像引用元: React hooksを基礎から理解する (useContext編) 1. コンテキストの作成 createContext と useContext をインポート src/index.tsx import React, { useReducer, memo, Dispatch, createContext, useContext, } from 'react'; createContext の構文 const MyContext = React.createContext(defaultValue); 2. AppContext の作成 ここでは、前項では props として各コンポーネントへ渡す必要があった State と Dispatch を保持するコンテキストとする。 src/index.tsx const AppContext = createContext( {} as { state: State; dispatch: Dispatch<Action>; } ); 3. Context の提供元(=親コンポーネント)での設定 (1) return 文の JSX を AppContext.Provider でラップする。 src/index.tsx const App: React.VFC = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <AppContext.Provider value={{ state, dispatch }}> <div className="container"> <Selector dispatch={dispatch} /> {state.filter === 'removed' ? ( <EmptyButton dispatch={dispatch} /> ) : ( <Form state={state} dispatch={dispatch} /> )} <FilteredTodos state={state} dispatch={dispatch} /> </div> </AppContext.Provider> ); }; 4. Context の提供元(=親コンポーネント)での設定 (2) 従前の props に代わって、 AppContext.Provider が state の値や dispatch メソッドを提供するため、各コンポーネントへの props は削除する。 src/index.tsx return ( <AppContext.Provider value={{ state, dispatch }}> <div className="container"> <Selector /> {state.filter === 'removed' ? ( <EmptyButton /> ) : ( <Form /> )} <FilteredTodos /> </div> </AppContext.Provider> ); 5. Context の提供を受ける各コンポーネントでの設定 useContext の構文 コンテクストオブジェクト(React.createContext からの戻り値)を受け取り、そのコンテクストの現在値を返す。 const value = useContext('コンテキストオブジェクト'); useContext フックの引数に AppContext を与えることで、そこから提供されるコンテキスト (= State, Dispatch) を利用できるので props は削除する。 index.tsx const Form: React.VFC = memo(() => { const { state, dispatch } = useContext(AppContext); const handleOnSubmit = ( e: React.FormEvent<HTMLFormElement | HTMLInputElement> ) => { e.preventDefault(); dispatch({ type: 'submit' }); }; const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { dispatch({ type: 'change', value: e.target.value }); }; return ( <form className="form" onSubmit={handleOnSubmit}> <input className="text" type="text" disabled={state.filter === 'checked'} value={state.text} onChange={handleOnChange} /> <input className="button" type="submit" disabled={state.filter === 'checked'} value="追加" onSubmit={handleOnSubmit} /> </form> ); });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

useEffect 第二引数の使い分け

投票アプリ作成中、よくわからない警告に遭遇(Part2) 投票アプリ以外でも、時々、今回の警告は見たことがあった。。。 React Hook useEffect has a missing dependency: 'email'. Either include it or remove the dependency array react-hooks/exhaustive-deps (日本語)React Hook useEffectに依存関係がありません: 'email'。それを含めるか、依存関係配列react-hooks / exhaustive-depsを削除します 大前提として、useEffectについて 第一引数には、副作用として実行する関数を与える。 useEffectに指定することで、副作用は、コンポーネントの描写が終わった後に実行される。 第二引数によって、副作用を実行する頻度を設定できる。 第二引数を与えなかった場合、コンポーネントの描写後、毎回実行。 ?第二引数に空の配列を与えた場合には、初回描写時にのみ実行。 ?第二引数に配列を与え、要素として変数を指定すると、指定した変数に変更があった場合のみ実行。 警告が出る理由 第2引数を指定していないから。 今回、以下のコードのように、 ?第二引数に空の配列を与えた場合には、初回描写時にのみ実行。 としていた。 // ユーザー情報を取得 useEffect(() => { firebase .firestore() .collection("users") .where("email", "==", email) .onSnapshot((snapshot) => { const getUsers = snapshot.docs.map((doc) => { return { ...doc.data(), docid: doc.id, }; }); setUsers(getUsers); }); }, []); 対処法 以下のコードのように、 ?第二引数に配列を与え、要素として変数を指定すると、指定した変数に変更があった場合のみ実行。 とすることで、警告は消えた。 }, [email]); その他の警告と対処 ① React Hook useEffect has missing dependencies: 'history.location.state.birthday', 'history.location.state.email', 'history.location.state.gender', 'history.location.state.password', and 'history.location.state.username'. Either include them or remove the dependency array react-hooks/exhaustive-deps (対処) こちらも ?第二引数を空の配列 としていたため、 以下のように、 ?変数を指定した ことで、警告は消えた。 }, [history]); ② React Hook useEffect has missing dependencies: 'email', 'question.answer1Id', 'question.answer2Id', and 'question.docid'. Either include them or remove the dependency array react-hooks/exhaustive-deps (対処) こちらも ?第二引数を空の配列 としていたため、 以下のように、 ?変数を指定した ことで、警告は消えた。 }, [email, question]); 今後は、、、 第二引数が、 ⭐空の配列が適している場合、変数を指定する場合⭐ これらを適切に使い分けれるように、まだまだ勉強が必要だ。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

useEffect 第二引数について

投票アプリ作成中、よくわからない警告に遭遇(Part2) 投票アプリ以外でも、時々、今回の警告は見たことがあった。。。 React Hook useEffect has a missing dependency: 'email'. Either include it or remove the dependency array react-hooks/exhaustive-deps (日本語)React Hook useEffectに依存関係がありません: 'email'。それを含めるか、依存関係配列react-hooks / exhaustive-depsを削除します 大前提として、useEffectについて 第一引数には、副作用として実行する関数を与える。 useEffectに指定することで、副作用は、コンポーネントの描写が終わった後に実行される。 第二引数によって、副作用を実行する頻度を設定できる。 第二引数を与えなかった場合、コンポーネントの描写後、毎回実行。 ?第二引数に空の配列を与えた場合には、初回描写時にのみ実行。 ?第二引数に配列を与え、要素として変数を指定すると、指定した変数に変更があった場合のみ実行。 警告が出る理由 第2引数を指定していないから。 今回、以下のコードのように、 ?第二引数に空の配列を与えた場合には、初回描写時にのみ実行。 としていた。 // ユーザー情報を取得 useEffect(() => { firebase .firestore() .collection("users") .where("email", "==", email) .onSnapshot((snapshot) => { const getUsers = snapshot.docs.map((doc) => { return { ...doc.data(), docid: doc.id, }; }); setUsers(getUsers); }); }, []); 対処法 以下のコードのように、 ?第二引数に配列を与え、要素として変数を指定すると、指定した変数に変更があった場合のみ実行。 とすることで、警告は消えた。 }, [email]); その他の警告と対処 ① React Hook useEffect has missing dependencies: 'history.location.state.birthday', 'history.location.state.email', 'history.location.state.gender', 'history.location.state.password', and 'history.location.state.username'. Either include them or remove the dependency array react-hooks/exhaustive-deps (対処) こちらも ?第二引数を空の配列 としていたため、 以下のように、 ?変数を指定した ことで、警告は消えた。 }, [history]); ② React Hook useEffect has missing dependencies: 'email', 'question.answer1Id', 'question.answer2Id', and 'question.docid'. Either include them or remove the dependency array react-hooks/exhaustive-deps (対処) こちらも ?第二引数を空の配列 としていたため、 以下のように、 ?変数を指定した ことで、警告は消えた。 }, [email, question]); 今後は、、、 第二引数が、 ⭐空の配列が適している場合、変数を指定する場合⭐ これらを適切に使い分けれるように、まだまだ勉強が必要だ。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.jsとNext.jsについて

React.jsとNext.jsについて勉強しているので、その情報を共有するために投稿します。 solidityとスマートコントラクトを勉強していたが、React.jsと組み合わせてアプリケーションを作成している例をたくさん見かけたこともあり、 React.jsもこの際理解しようということにしました。 違いは下記の通り。 React.js: SPAを考えて作成されている。HTMLをJavaScriptの中に記述できるJSX機能が強力! Next.js:React.jsに各種ライブラリを統合してパッケージ化したもの。React.jsを拡張させることができる! ソースコードは、下記GitHubで公開中 react_app next_app どちらも npx コマンドを利用することで土台部分を自動的に作成してくれるため、すぐに開発に入ることができるという強みを持っている! 要素をコンポーネント化することで、複雑な画面でもプラモデルもパーツを組み立てる感覚で開発できるところが面白いし、扱いやすいと感じた。 エンタープライズ向けのWebページでもおそらく課題になるであろう画面レイアウトの統一とも相性がものすごく良いとも考えている。 ただ、従来のHTMLやJSPと比べてかなり書き方が変わるため、いきなり導入するということはかなりハードルが高そうだが、 かなり柔軟に開発できるためメリットの方が大きいのではないかと考えている。 要素をコンポーネント化することで、複雑な画面でもプラモデルのパーツを組み立てる感覚で開発できるところが面白いし、扱いやすいと感じた。 エンタープライズ向けのWebページでもおそらく課題になるであろう画面レイアウトの統一とも相性がものすごく良いとも考えている。 ただ、従来のHTMLやJSPと比べてかなり書き方が変わるため、いきなり導入するということはハードルが高そうだが、柔軟に開発できるため導入するメリットの方が大きいのではないかと考えている。(特にNext.jsについては、HTMLファイルが無くなり、全てJavaScriptでの記述となるため従来のやり方に慣れている方からには、少なからず抵抗があるかもしれません。。。) ※現在、勉強中のため、適宜追記していきたいと考えています。 以下、参考にした書籍となります。 実践スマートコントラクト開発 node.js超入門 React.js&Next.js超入門
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Reat × Rails(api)、フロントとバックエンドを分けるとはどういう事か理解する】

実務未経験の自分が、 現在、Rails(api)とReact.jsでポートフォリオを作成中ですが、 このようなフロントエンドとバックエンドを分けて開発する事の概念的な部分に関して、 イメージがしづらかったので、アウトプットとして整理しておきます。 ※めちゃくちゃ初心者向けの内容になるかと思います。 そもそもフロントエンドとバックエンドを分けるってどういう事?? 凄く簡単に言うと、Railsにおけるviewに当たる部分を、別のフレームワークを使って開発すると理解しています。 Railsでアプリを何個か作成していた為、いわゆるviewにあたるファイルが、〇〇.erbという形式で作成しておりました。 しかし、Rails apiモードではviewというディレクトリはありませんし、.erbというファイルも作成しません。 ※正確にはviewディレクトリはありますが、ここでuserとかpostだったりのディレクトリは作成しません。 一般的には、このviewというディレクトリの代わりに、front又はfrontendというディレクトリでReactやVueを使って作成していきます。 ※viewの代わりにという言い方は語弊あるかもしれませんが、このような理解で良いかと思います。 viewの部分を置き換えるって何?? 当たり前ですが、viewの部分が担っていた役割を、そのままReactやVueが行っていく事になります。 何が変わるのかというと、 基本的にページ遷移した際の画面をリロードする必要が無くなり、 ユーザー(クライアント)側の使い勝手が良くなります。 ※この辺はこちらの記事を読むと良いです。 今さら聞けない!シングルページアプリケーションとは ディレクトリ構造的には、こんな感じになります。 ※frontendのディレクトリ構造は、ReactやVueのチュートリアルや記事を見て下さい。 Railsにおけるviewの主な役割って何だったっけ? ブラウザ画面の描画 何らかのリクエストを投げて、返ってきた値を受け取る 例えば、下記のような記述があった場合 view/user/show.html.erb <p>名前</p> <%= @user.name %> config/routes.rb resources :users, only[:show] controllers/users_controller.rb @user = User.find(params[:id]) まず、 show.html/erbから@userの情報を下さい!というリクエストをresources :user, only[:show]というルーティング宛に送ります。 ※どのルーティングが判別するかはRailsが判断してくれています。 次に、 routes.rbがuser/show.html.erbから来たリクエストは、どのコントローラーのどのアクションに渡すべきか判別します。 最後に controllers/users_controller.rbが受け取ったリクエストを元にUserモデルから該当userを探して、レスポンスを返す という流れになっているかと思います。 フロントエンドとバックエンドを分けての開発の場合は、 このview/user/show.html.erbが@userの情報を下さい!というリクエストを投げる行為を、ReactやらVueやらで行っていきます。 ちなみに、このリクエストを投げる行為の事をapiを叩くと言ったりしています。 ※今回の例はGETリクエストですが。 フロントを分けるとどういう動きになるの?? 実際の記述は下記のようになります。 かなり省略して書いておりますが、流れを掴めれば良いと思っているので、ご容赦下さい。 UserShow.jsx get(lokalhost3000/users/:id) ... return( {user.name} ) ※jsxとはReact.jsのファイル端子ですが、Railsでいうviewにあたる部分と思って下さい。 routes.rb resources :users, only[:show] users_controller.rb def show @user = User.find(params[:id]) render json: { user: @user } end 流れの簡単な説明 ルーティングを指定してGETリクエストを投げる まず、フロントエンドのviewからRailsに向けてuserの情報を下さいとgetリクエストを投げています。 ※jsxの記述はかなりハショッております。 Railsでは、@user.nameと記述すれば勝手にRailsがリクエストを投げて値を返すところまで、やってくれていましたが、 フロントを分ける場合は、このようにリクエストを投げる処理を自分で実装しなければなりません。 ※localhost3000/users/:idはrails routesに記載されているルーティングの事です。 また、Railsでよく使っていた、user_pathのようなルーティング指定も使いません。 ※使う方法もあるのかもしれませんが、自分は知りません。。 ルーティングが実行すべきコントローラーを判断する ここはRails単体の動きとなんら変わらないので、パスします。 コントローラーがリクエストを受け取って値を返す 見慣れない書き方が、render json: { user: @user }こちらかと思います。 Rails単体の場合は、@user = User.find(params[:id])だけでも良いのですが、 フロントを分ける場合は、この@userの値をjson形式というデータ形式に変えて渡す必要があります。 jsonとは、簡単に言うとJavascriptのデータ形式と思って頂ければ良いかなと。 詳しく知りたい方は下記を参照下さい。 https://products.sint.co.jp/topsic/blog/json なぜ、こんな形式に変換しないといけない? 変換をしないままでは、rubyのデータ形式のままでReactやVueはJavaScriptで書くので、 rubyのデータ形式では対応出来ないからです。 Rails単体の場合は、.erbもrubyを元に作成される為、そのままでも対応出来るという事です。 最後に 自分用のアウトプットとはいえ、かなり煩雑に書いてしまったので、 指摘や分かりづらい点があれば、どんどんご指摘下さい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Warning: Can't perform a React state update on an unmounted component.

投票アプリ作成中、よくわからない警告に遭遇 投票作成ページにて、投票を作成すると、投票を表示させるメインページに戻るが、その際に毎回、以下のような警告が出ていた。 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. (日本語)警告:マウントされていないコンポーネントでReact状態の更新を実行することはできません。 これは何もしませんが、アプリケーションのメモリリークを示しています。 修正するには、useEffectクリーンアップ関数のすべてのサブスクリプションと非同期タスクをキャンセルします。 警告が出る理由 useEffet内で、まだ、firebaseからすべて読み込まれていないのに、setQuestionsを実行しようとするから警告が出る。 const VotesList = () => { const [questions, setQuestions] = useState(null); useEffect(() => { firebase .firestore() .collection("questions") .orderBy("timestamp", "desc") .onSnapshot((snapshot) => { const questions = snapshot.docs.map((doc) => { return { username: doc.data().username, question: doc.data().question, answer1: doc.data().answer1, answer1Id: doc.data().answer1Id, answer2: doc.data().answer2, answer2Id: doc.data().answer2Id, docid: doc.id, //<- keyを設定するためにidを取得、あとで削除機能等をつける }; }); setQuestions(questions); }); }, []); return ( <> <ul> {questions?.map((question) => { return <CountVotes question={question} key={question.docid} />; })} </ul> </> ); }; 対処法 下記のように書き直すことで、警告は出なくなった。 アンマウント時にはsetQuestionsは実行せず、 マウント時のみsetQuestionsが実行するようにする。 まだDOMに無い時には return dispose で一度回避するイメージ。 useEffect(() => { let disposed = false; const dispose = () => { disposed = true; }; firebase .firestore() .collection("questions") .orderBy("timestamp", "desc") .onSnapshot((snapshot) => { const questions = snapshot.docs.map((doc) => { return { username: doc.data().username, question: doc.data().question, answer1: doc.data().answer1, answer1Id: doc.data().answer1Id, answer2: doc.data().answer2, answer2Id: doc.data().answer2Id, docid: doc.id, //<- keyを設定するためにidを取得、あとで削除機能等をつける }; }); !disposed && setQuestions(questions); }); return dispose; }, []);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「SolidityとEthereumによる実践スマートコントラクト開発―Truffle Suiteを用いた開発の基礎からデプロイまで」でハマった時の解決策

truffle unbox reactが失敗する問題 「9.1.1 開発環境のセットアップ」 Nvmを使って、Nodeのバージョンを色々変えてみましょう npm ERR! code 1 npm ERR! path /Users/kobayashitomoya/fundraiser/fundraiser-dapp/client/node_modules/sha3 npm ERR! command failed npm ERR! command sh -c node-gyp rebuild npm ERR! CXX(target) Release/obj.target/sha3/src/addon.o npm ERR! gyp info it worked if it ends with ok npm ERR! gyp info using node-gyp@8.0.0 npm ERR! gyp info using node@15.6.0 | darwin | x64 npm ERR! gyp info find Python using Python version 3.8.2 found at "/Library/Developer/CommandLineTools/usr/bin/python3" npm ERR! gyp info spawn /Library/Developer/CommandLineTools/usr/bin/python3 nvm install v12.1.0 nvm use v12.1.0 truffle develop → compileが失敗する問題 p.172 Solidityのバージョンが異なるのが問題 バージョンを合わせましょう ・fundraiser-dapp/contracts/Fundraiser.sol ・fundraiser-dapp/contracts/FundraiserFactory.sol ・fundraiser-dapp/contracts/Migrations.sol ・fundraiser-dapp/contracts/SimpleStorage.sol - pragma solidity >=0.4.21 <0.7.0; + pragma solidity >=0.4.21 <0.9.0; ・fundraiser-dapp/truffle-config.js }, // Configure your compilers compilers: { solc: { version: "0.8.0", // Fetch exact version from solc-bin (default: truffle's version) // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) // settings: { // See the solidity docs for advice about optimization and evmVersion // optimizer: { // enabled: false, // runs: 200 // }, // evmVersion: "byzantium" // } } }, Metamaskが繋がらない! p.84 「9.2.4 Reactを使ってあいさつ文を設定する」 Metamaskを再インストールしましょう https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=ja
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React連携(2)

app/javascript/packs/hello_react.jsx // Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file, // like app/views/layouts/application.html.erb. All it does is render <div>Hello React</div> at the bottom // of the page. import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' const Hello = props => ( <div>Hello {props.name}!</div> ) Hello.defaultProps = { name: 'David' } Hello.propTypes = { name: PropTypes.string } document.addEventListener('DOMContentLoaded', () => { ReactDOM.render( <Hello name="React" />, document.body.appendChild(document.createElement('div')), ) }) import importはファイルの読み込みです。 【1】react 「react」はユーザーインターフェースを構築するためのJavaScriptライブラリで、Reactのコンポーネントを定義するために必要な機能のみが含まれています。 【2】react-dom 「react-dom」は、Reactのための、DOMの操作とサーバーレンダリング機能を提供します。 【3】prop-types 「prop-types」は、Propの型情報をチェックできるようにするツールです。 定数の宣言 constを利用して定数を宣言します。 hello_react.jsx const Hello = props => ( <div>Hello {props.name}!</div> ) Helloという定数に、<div>Hello {props.name}!</div>が代入されます。 つまり「定数Helloにアロー関数を代入している(引数がprops)」 →引数が1つなら(  )も不要 (引数0なら()は必要)。なのでpropsは(  )なし 通常関数 function ( ) { } アロー関数arrow function ( ) => { } *書き換え可能 デフォルトpropsの宣言 以下のコードでは、propsのデフォルト値に‘David’を設定しています。(コンポーネントの属性設定) hello_react.jsx Hello.defaultProps = { name: 'David' propTypesにて型の制約の指定 propsのnameに入る型を指定しています。指定することによって、値を受け取った際に、肩のチェックを行ってくれます。 (コンポーネントの属性設定) hello_react.jsx Hello.propTypes = { name: PropTypes.string } *PropTypesで渡された値の型チェックを行い、stringに限定している 「Hello React!」の表示 hello_react.jsx document.addEventListener('DOMContentLoaded', () => { ReactDOM.render( <Hello name="React" />, document.body.appendChild(document.createElement('div')), ) }) ReactDOM.render( レンダリングする内容【第1引数】,表示を割り当てる場所【第2引数】) addEventListener( )は、様々なイベント処理を実行することができるメソッド。上記はDOMContentLoadedとあるので、DOMツリーの構築が完了した時点(ロードされた時点)で呼ばれます。(=WEBページの読み込みが終わってJavaScriptのスクリプトが実行できる状態になったら)Helloオブジェクトのpropsのnameに「React」という文字列が代入されます。 最後のdocument.body.appendChild(document.createElement(‘div’))により、divタグで囲まれた<Hello name=”React” />の内容が、bodyタグが終了する直前に追加されます。 *document.createElement(‘div’)はdiv要素を生成 *body.appendChildはbodyの最後にbodyの子要素追加するというもの。 ここがむっちゃ詳しいです @kouheiszkさんRailsチュートリアル幻の15章 React編 https://qiita.com/kouheiszk/items/c85e70e331ba75841818
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js + TypeScript + Material-UI で静的HTMLをエクスポートするまで

はじめに 仕事で Next.js を使っているので、開発環境を初期構築する方法をメモします。 TypeScript と Material-UI を含めた鉄板ネタ?で簡単に環境構築できます。 前提 パッケージマネージャーとして yarn をインストールしています。 Next.js React をベースにしたフレームワークで、SSRやらSSGが簡単に実現できます。 その中でも私はファイルベースのルーティングが好きなので、SSRやらSSGが必要なくても Next.js を選択したいものです。 Next.js の詳細はこちらの公式ページを参照。 TypeScript 言わずもがな JavaScript に静的な型付けができます。 Material-UI 私のようなデザインセンスのないエンジニアでも、簡単にカッコいいWebデザインを作れる(かもしれない)UIコンポーネントを提供してくれます。 Material-UI の詳細はこちらの公式ページを参照。 Next.js のプロジェクトを構築する まず Next.js のプロジェクトを構築していきます。 React の create-react-app と同じように create-next-app で簡単に初期構築できます。 また、サンプルがたくさん用意されているので、自分の目的に合わせて近しいプロジェクトを選択して初期構築することができます。 例えば今回だと Material-UI のプロジェクトを作ってみようと思います。 はい。ありましたね。 サンプルから初期構築する場合は -e オプションを使います。 npx create-next-app -e with-material-ui my-material-ui てな感じです。簡単ですね。 ではやってみます。 $ npx create-next-app --example with-material-ui my-material-ui Could not locate an example named "with-material-ui". Please check your spelling and try again. 失敗しました! サンプルは既にこちらに移動されているようです。なるほど。 仕方ないので別のサンプルを使ってみます。 $ npx create-next-app -e with-typescript-eslint-jest my-nextjs Creating a new Next.js app in /../my-nextjs. Downloading files for example with-typescript-eslint-jest. This might take a moment. Installing packages. This might take a couple of minutes. added 1099 packages, and audited 1100 packages in 36s 106 packages are looking for funding run `npm fund` for details found 0 vulnerabilities Initialized a git repository. Success! Created my-nextjs at /../my-nextjs Inside that directory, you can run several commands: npm run dev Starts the development server. npm run build Builds the app for production. npm start Runs the built app in production mode. We suggest that you begin by typing: cd my-nextjs npm run dev 今度は成功しました! yarn run dev 後に localhost:3000 にアクセスして以下のページが表示されました。 Material-UI を追加する ここにある通り、手動で追加することにします。 $ yarn add @material-ui/core yarn add v1.22.10 info No lockfile found. warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json. [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... [4/4] ? Building fresh packages... success Saved lockfile. success Saved 223 new dependencies. ・・・ pages/index.tsx を以下のようにして Material-UI を適用してみます。 pages/index.tsx import Head from 'next/head' import Image from 'next/image' import AppBar from '@material-ui/core/AppBar' import Toolbar from '@material-ui/core/Toolbar' import Typography from '@material-ui/core/Typography' import Container from '@material-ui/core/Container' import { makeStyles } from '@material-ui/core/styles' import CssBaseline from '@material-ui/core/CssBaseline' const useStyles = makeStyles((theme) => ({ root: { display: 'flex', flexDirection: 'column', minHeight: '100vh', }, main: { marginTop: theme.spacing(8), marginBottom: theme.spacing(2), }, footer: { padding: theme.spacing(3, 2), marginTop: 'auto', backgroundColor: theme.palette.primary.main, color: 'white', textAlign: 'center', }, })) export const Home = (): JSX.Element => { const classes = useStyles() return ( <div className={classes.root}> <CssBaseline /> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <header> <AppBar position="relative"> <Toolbar> <Typography variant="h6" color="inherit" noWrap> {"Material-UI"} </Typography> </Toolbar> </AppBar> </header> <main className={classes.root}> <Container component="main" className={classes.main} maxWidth="md"> <h1 className="title"> Welcome to <a href="https://nextjs.org">Next.js!</a> </h1> <p className="description"> Get started by editing <code>pages/index.tsx</code> </p> <button onClick={() => { window.alert('With typescript and Jest') }} > Test Button </button> <div className="grid"> <a href="https://nextjs.org/docs" className="card"> <h3>Documentation &rarr;</h3> <p>Find in-depth information about Next.js features and API.</p> </a> <a href="https://nextjs.org/learn" className="card"> <h3>Learn &rarr;</h3> <p>Learn about Next.js in an interactive course with quizzes!</p> </a> <a href="https://github.com/vercel/next.js/tree/master/examples" className="card" > <h3>Examples &rarr;</h3> <p>Discover and deploy boilerplate example Next.js projects.</p> </a> <a href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" className="card" > <h3>Deploy &rarr;</h3> <p>Instantly deploy your Next.js site to a public URL with Vercel.</p> </a> </div> </Container> </main> <footer className={classes.footer}> <a href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" target="_blank" rel="noopener noreferrer" > Powered by{' '} <Image src="/vercel.svg" alt="Vercel Logo" height={'32'} width={'64'} /> </a> </footer> <style jsx>{` // 変更なしのため割愛 ・・・ `}</style> </div> ) } export default Home 補足 <style> の中身は初期状態から変更なし。長いので省略。 長くなってしまったので、本来であればコンポーネントを分割すべきでしょうね。 画面はこんな感じです。 ただ、コードを修正してページ更新してみるとブラウザのコンソールにこんなエラーが。。。 Warning: Prop `className` did not match. Server: "MuiContainer-root makeStyles-main-5 MuiContainer-maxWidthMd" Client: "MuiContainer-root makeStyles-main-2 MuiContainer-maxWidthMd" at main at Container (webpack-internal:///./node_modules/@material-ui/core/esm/Container/Container.js:85:23) at WithStyles (webpack-internal:///./node_modules/@material-ui/styles/esm/withStyles/withStyles.js:61:31) at main at div at Home (webpack-internal:///./pages/index.tsx:58:17) at App (webpack-internal:///./node_modules/next/dist/pages/_app.js:77:5) at ErrorBoundary (webpack-internal:///./node_modules/@next/react-dev-overlay/lib/internal/ErrorBoundary.js:23:47) at ReactDevOverlay (webpack-internal:///./node_modules/@next/react-dev-overlay/lib/internal/ReactDevOverlay.js:73:23) at Container (webpack-internal:///./node_modules/next/dist/client/index.js:155:5) at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:643:24) at Root (webpack-internal:///./node_modules/next/dist/client/index.js:779:25) そこで先程紹介した移動したサンプルを参考に、以下のファイルを追加します。 追加したファイル pages/_app.tsx pages/_document.tsx pages/_app.tsx import React from 'react' import Head from 'next/head' import { AppProps } from 'next/app' import CssBaseline from '@material-ui/core/CssBaseline' const App: React.FC<AppProps> = ({ Component, pageProps }) => { React.useEffect(() => { // Remove the server-side injected CSS. const jssStyles = document.querySelector('#jss-server-side') if (jssStyles && jssStyles.parentElement) { jssStyles.parentElement.removeChild(jssStyles) } }, []) return ( <React.Fragment> <CssBaseline /> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <Component {...pageProps} /> </React.Fragment> ) } export default App _app.tsx の補足 全ページ共通の処理を実装できますが、今回のエラーとは特に関係なさそう。 だけどあると便利なので、pages/index.tsx で実装した <CssBaseLine /> と <Head> はこちらに移動してしまう。 pages/_document.tsx import { ServerStyleSheets } from '@material-ui/core/styles' import Document, { Head, Html, Main, NextScript } from 'next/document' import React from 'react' export default class MyDocument extends Document { render(): JSX.Element { return ( <Html lang="ja-JP"> <Head> {/* PWA primary color */} <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ) } } // `getInitialProps` belongs to `_document` (instead of `_app`), // it's compatible with server-side generation (SSG). MyDocument.getInitialProps = async (ctx) => { // Resolution order // // On the server: // 1. app.getInitialProps // 2. page.getInitialProps // 3. document.getInitialProps // 4. app.render // 5. page.render // 6. document.render // // On the server with error: // 1. document.getInitialProps // 2. app.render // 3. page.render // 4. document.render // // On the client // 1. app.getInitialProps // 2. page.getInitialProps // 3. app.render // 4. page.render // Render app and page and get the context of the page with collected side effects. const sheets = new ServerStyleSheets() const originalRenderPage = ctx.renderPage ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheets.collect(<App {...props} />), }) const initialProps = await Document.getInitialProps(ctx) return { ...initialProps, // Styles fragment is rendered after the app and page rendering finish. styles: [ ...React.Children.toArray(initialProps.styles), sheets.getStyleElement(), ], } } _document.tsx の補足 エラー解消にはこちらのファイルの MyDocument.getInitialProps の定義のところが重要です。 余談ですがクラスコンポーネントになってますね。こちらのファイルはサンプルをほぼ丸コピしてます。 これでコンソールにエラーは出なくなりました! 静的HTMLを出力する Next.js には next export というコマンドが用意されており、簡単に静的なHTMLを出力できます。 そこで package.json を以下のように修正して、ビルド時に静的HTMLを出力するようにします。 diff --git a/package.json b/package.json index 64f8a47..d2ecd0d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "1.0.0", "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build && next export", "start": "next start", "type-check": "tsc --pretty --noEmit", "format": "prettier --write .", @@ -26,6 +26,7 @@ ] }, "dependencies": { "@material-ui/core": "^4.11.4", "next": "latest", "react": "^17.0.1", "react-dom": "^17.0.1" yarn run build を実行します。 $ yarn run build yarn run v1.22.10 $ next build && next export info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5 info - Checking validity of types info - Using external babel configuration from /../my-nextjs/.babelrc info - Creating an optimized production build info - Compiled successfully info - Collecting page data info - Generating static pages (3/3) info - Finalizing page optimization Page Size First Load JS ┌ ○ / 10.3 kB 97.2 kB ├ /_app 0 B 86.9 kB ├ ○ /404 3.71 kB 90.6 kB └ λ /api/hello 0 B 86.9 kB + First Load JS shared by all 86.9 kB ├ chunks/205.fc6391.js 7.94 kB ├ chunks/433.83deda.js 12.6 kB ├ chunks/894.a1bad4.js 22 kB ├ chunks/framework.aa6452.js 42.4 kB ├ chunks/main.5aaba5.js 154 B ├ chunks/pages/_app.f0c4dc.js 801 B └ chunks/webpack.231c1c.js 993 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps) (ISR) incremental static regeneration (uses revalidate in getStaticProps) info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5 info - using build directory: /../my-nextjs/.next info - Copying "static build" directory info - No "exportPathMap" found in "next.config.js". Generating map from "./pages" Error: Image Optimization using Next.js' default loader is not compatible with `next export`. Possible solutions: - Use `next start` to run a server, which includes the Image Optimization API. - Use any provider which supports Image Optimization (like Vercel). - Configure a third-party loader in `next.config.js`. - Use the `loader` prop for `next/image`. Read more: https://nextjs.org/docs/messages/export-image-api at /../my-nextjs/node_modules/next/dist/export/index.js:14:785 at async Span.traceAsyncFn (/../my-nextjs/node_modules/next/dist/telemetry/trace/trace.js:6:584) error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. はい。失敗します! どうやら原因はここにあるようです。 pages/index.tsx import Image from 'next/image' ・・・ <Image src="/vercel.svg" alt="Vercel Logo" height={'32'} width={'64'} /> こんなIssueがありますね。 どうやらnext/imageは、画像の表示をサーバー側で最適化してくれるコンポーネントっぽいです。 今回は特に不要なので React の <img> に置き換えました。 pages/index.tsx <img src="/vercel.svg" alt="Vercel Logo" height={'32'} width={'64'} /> 再び yarn run build を実行します。 $ yarn run build yarn run v1.22.10 $ next build && next export info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5 info - Checking validity of types info - Using external babel configuration from /../my-nextjs/.babelrc info - Creating an optimized production build info - Compiled successfully info - Collecting page data info - Generating static pages (3/3) info - Finalizing page optimization Page Size First Load JS ┌ ○ / 7.35 kB 93.8 kB ├ /_app 0 B 86.5 kB ├ ○ /404 3.71 kB 90.2 kB └ λ /api/hello 0 B 86.5 kB + First Load JS shared by all 86.5 kB ├ chunks/262.b5f9f9.js 21.7 kB ├ chunks/284.ad3787.js 7.54 kB ├ chunks/433.d1602d.js 12.9 kB ├ chunks/framework.aa6452.js 42.4 kB ├ chunks/main.b89501.js 154 B ├ chunks/pages/_app.c8ce47.js 802 B └ chunks/webpack.1bcf44.js 993 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps) (ISR) incremental static regeneration (uses revalidate in getStaticProps) info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5 info - using build directory: /../my-nextjs/.next info - Copying "static build" directory info - No "exportPathMap" found in "next.config.js". Generating map from "./pages" info - Launching 3 workers warn - Statically exporting a Next.js application via `next export` disables API routes. This command is meant for static-only hosts, and is not necessary to make your application static. Pages in your application without server-side data dependencies will be automatically statically exported by `next build`, including pages powered by `getStaticProps`. Learn more: https://nextjs.org/docs/messages/api-routes-static-export info - Copying "public" directory info - Exporting (2/2) Export successful. Files written to /../my-nextjs/out ✨ Done in 15.24s. 今度は成功しました! 念の為動作を確認します。 $ cd out $ npx http-server Starting up http-server, serving ./ Available on: http://127.0.0.1:8080 http://192.168.100.49:8080 Hit CTRL-C to stop the server localhost:8080 にアクセスして以下のページが表示されました。 あとは S3 やら Firebase Hosting やら適当にデプロイしてくだされ。 まとめ Next.js で Material-UI を使いたい場合は create-next-app ではなく、このページのサンプルをクローンした方が早そう。 create-next-app で初期構築したプロジェクトに後から Material-UI を入れる場合はこのページのサンプルから _app.tsx、_document.tsx を忘れずにパクる。 静的HTMLを出力する場合は next/image コンポーネントのご利用は慎重に。 ちなみに pages ディレクトリは src 配下に移動してもルーティングとして認識してくれます。私は src 配下に pages がある方が好きだな。 参考にしたサイト Next.js & material UIで[Prop className did not match]が発生する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactとの連携(1)

今回は「index.html.erb」表示まで react用アプリケーション作成 rails rails new RailsReactApp --webpack=react indexページ作る rails rails g controller hello index app/controllers/hello_controller.rb class HelloController < ApplicationController def index end end indexファイルコーディング rails <%= javascript_pack_tag 'hello_react' %> <h1 class="display-4 text-primary"> Hello#index</h1> <div id="hello"></div> Viewファイル修正 app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title>RailsReactApp</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css bootstrap.css> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= yield %> </body> </html> Reactで表示完了 Reactを利用した処理が実行されています。 appフォルダ内の JavaScriptフォルダ内にはpacksというフォルダがあり、この中にhello_react.jsxというファイルが用意されています。このスクリプトが読み込まれています。 *標準でReactコンポーネントのJavaScriptファイルについて、拡張子を「.js」ではなく「.jsx」にすることが推奨されている。 今日はここまで
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む