- 投稿日:2020-08-12T23:26:52+09:00
VS CodeのスニペットでReactのコンポーネント雛形を作る
コンポーネントの雛形をVS Codeのスニペットに登録しておくと捗ります♪
スニペットの設定手順
- VS Codeの
Code > Preferences > User Snippets
tsxファイルのスニペットを作る場合
typescriptreact
と入力
VS Codeではスニペットをファイル種別ごとにjsonファイルで管理します。
以下をコピペ
以下はReact Nativeのコンポーネントの一例です。お好みで変更して下さい。{ "React Native Component": { "prefix": "react-native-component", "body": [ "import React from \"react\";", "import { View, StyleSheet } from \"react-native\";", "", "type Props = {};", "", "export const ${TM_FILENAME_BASE}: React.FC<Props> = ({}) => {", "\treturn (", "\t\t<View style={styles.container}>", "\t\t\t<View />", "\t\t</View>", "\t)", "};", "", "const styles = StyleSheet.create({", "\tcontainer: {}", "});" ], "description": "React Native component" } }スニペットの利用
エディタで
react-native-component
と打ち込んでTabキー。スニペットが挿入されます!
コンポーネント名はファイル名が自動的に反映されます。
- 投稿日:2020-08-12T21:10:06+09:00
React Tutorial 「改良のアイデア」の実装
React Tutorial
React公式のチュートリアルページです。
三目並べゲームを一通りハンズオンで実装します。
その後、改良のアイデアを提案されます。JavaScriptの練習がてら挑戦してみました。
時間がある場合や、今回身につけた新しいスキルを練習してみたい場合に、あなたが挑戦できる改良のアイデアを以下にリストアップしています。後ろの方ほど難易度が上がります:
1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
2. 着手履歴のリスト中で現在選択されているアイテムをボールドにする。
3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。ここから続きを実装しました。
ソースコード
開く
index.jsimport React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import classNames from "classnames"; function Square(props) { const squareClass = classNames("square", { "high-light": props.winnerSquare === 0 || props.winnerSquare }); return ( <button className={squareClass} onClick={props.onClick}> {props.value} </button> ); } class Board extends React.Component { renderSquare(i) { const winnerSquare = this.props.winnerSquares && this.props.winnerSquares.includes(i) ? i : null; return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} winnerSquare={winnerSquare} /> ); } render() { const boardRows = []; let k = 0; for (let i = 0; i < 3; i++) { const threeSquares = []; for (let j = 0; j < 3; j++) { threeSquares.push(this.renderSquare(k++)); } boardRows.push(<div className="board-row">{threeSquares}</div>); } return <div>{boardRows}</div>; } } class Game extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill(null), player: null, col: 0, row: 0, }, ], stepNumber: 0, xIsNext: true, choosedHistory: null, isAsc: true, }; } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares)[0] || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; const [col, row] = this.convertColAndRow(i); this.setState({ history: history.concat([ { squares: squares, player: squares[i], col: col, row: row, }, ]), stepNumber: history.length, xIsNext: !this.state.xIsNext, choosedHistory: null, }); } convertColAndRow(i) { const colAndRow = []; for (let i = 1; i <= 3; i++) { for (let j = 1; j <= 3; j++) { colAndRow.push([j, i]); } } return colAndRow[i]; } jumpTo(step) { this.setState({ stepNumber: step, choosedHistory: step, xIsNext: step % 2 === 0, }); } changeOrder() { this.setState({ isAsc: !this.state.isAsc, }); } render() { const history = this.state.history; const current = history[this.state.stepNumber]; const [winner, winnerSquares] = calculateWinner(current.squares); console.log(winnerSquares); const moves = history.map((step, move) => { const desc = move ? "Go to move #" + move : "Go to game start"; const info = step.player ? `player: ${step.player}, col: ${step.col}, row: ${step.row}` : ""; const button = (i) => <button className={move === this.state.choosedHistory ? "bold" : ""} onClick={() => this.jumpTo(i)}>{desc}</button> return ( <li key={move}> {button(move)} <span>{info}</span> </li> ); }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } let drawMessage; if (!winner && !current.squares.includes(null)) { drawMessage = "The result is a draw."; } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i) => this.handleClick(i)} winnerSquares={winnerSquares} /> </div> <div className="game-info"> <div> {status} <button className="toggle-btn" onClick={() => this.changeOrder()}> {this.state.isAsc ? "Desc" : "Asc"} </button> <span>{drawMessage}</span> </div> <ol className={this.state.isAsc ? "asc-list" : "desc-list"}>{moves}</ol> </div> </div> ); } } // ======================================== ReactDOM.render(<Game />, document.getElementById("root")); function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return [squares[a], lines[i]]; } } return [null, null]; }index.cssbody { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; } ol, ul { padding-left: 30px; } .board-row:after { clear: both; content: ""; display: table; } .status { margin-bottom: 10px; } .square { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } .square:focus { outline: none; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } span { margin-left: 6px; } .bold { font-weight: bold; } .toggle-btn { margin-left: 10px; color: white; background-color: black; border: none; border-radius: 7px; } ol { display: flex; } ol.asc-list { flex-direction: column; } ol.desc-list { flex-direction: column-reverse; } .high-light { background-color: yellow; }画面
懸念点
this.props.winnerSquares && this.props.winnerSquares.includes(i) ? i : null; props.winnerSquare === 0 || props.winnerSquareもう少しいい書き方がありそう。
- 投稿日:2020-08-12T20:23:27+09:00
Reactで簡単にアコーディオンを実装できるライブラリ
概要
HTMLでアコーディオンを実装する方法はいくつかあります。CSSだけで実装する方法もあれば、Bootstrapなどのフレームワークを利用する方法も挙げられます。今回はReactのライブラリで簡単にアコーディオンを実装できるライブラリreact-accessible-accordionを紹介します。
サンプル
アコーディオンの実装サンプルです。
SampleComponent.jsimport React from "react"; import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemButton, AccordionItemPanel, } from "react-accessible-accordion"; import "react-accessible-accordion/dist/fancy-example.css"; export default function SampleComponent() { return ( <div> <Accordion allowZeroExpanded> <AccordionItem> <AccordionItemHeading> <AccordionItemButton>ヘッダー</AccordionItemButton> </AccordionItemHeading> <AccordionItemPanel>内容</AccordionItemPanel> </AccordionItem> </Accordion> </div> ); }その他出来ること
- 詳細はこのデモページにまとめられています。
- 今回の私のサンプルではヘッダーが一つですが、ヘッダーを複数設定して一つしか開けないようにする、といった制御もできます。
- アコーディオンを開閉したときのイベントを拾えたり、アコーディオンのstateも取得できるようです。
- 投稿日:2020-08-12T19:52:08+09:00
自分用メモ ライフサイクル
はじめに
ライフサイクルについてインプットしたのでアウトプットしておきます!!
reactを学び始めてこのライフサイクルが一番自分にとって難しく理解に苦しんでいます!!
まだまだ理解の途中ですが、アウトプットして行きながら理解を深めておきたいと思います!!至らない点など数あると思いますが、アドバイスしていただけると幸いです!!
ライフサイクル
これはreactで学ぶ上で最初に教えてもらったことで、これであればなんとなく理解できました!
mounting
・constructor
・render
・componentDidMount
UI(ユーザー表示)のための準備
これらが主要メソッドで上からの順でメソッドが呼ばれる!!updating
・render
・componentDidUpdate
ユーザーの処理などによりUIを更新、変化行う
ユーザーが操作できる箇所
ここの間はstate,propsが更新変更されるたびに何度もサイクルされる!!unmounting
・componentWillUnmount
他のコンポーネントに切り替えたりする時に現在のコンポーネントを削除する箇所注意)すいません!今記述した以外にもメソッドは多くあります。今自分がなんとか理解できてるメソッドのみ書かせていただきました。
詳しく知りたい方は是非こちらを見ていただくようお願いいたします!ライフサイクルメソッド
ユーザー表示するまでのサイクル
この中で必ず使うのはrenderのみになるので使わないのであれば使わなくてもいい!!
componentDidMount
mountingの時に呼び出されるメソッド
renderの後に呼ばれ、1回のみ呼ばれるメソッド
データを取ってくる場合や、タイマーやアニメーションをつけたりする時に使う!render
mounting/updateの時に呼ばれるメソッドで一番呼ばれ、唯一必ず記載しなければならないメソッド!!
render内のコードがUI(ユーザー表示)になるもの
props,stateが変更されるたびに呼び出されるものになる!!
注意)renderの中で直接state,propsの変更を行ってはいけない!してしまうとrender内ではstate,propsを変更するとメソッドが呼ばれるのでrenderが無限にループされてしまう!!componentDidUpdate
updateの時に使う
コンポーネント更新後に行われるメソッド
props,stateが変更時に呼び出される!!
多く呼び出されるメソッドなのでできるだけstate,propsの更新を行う思い処理は書かない!!componentWillMount
unmountの時に呼ばれるメソッド
アニメーションやタイマーをセットしていた場合ここで解除する
リソースの解除を行う時に他のコンポーネントに影響を及ぼさないようにする!
ここでprops,stateの更新、変更はできない!unmount時はrenderが呼ばれないから!!終わりに
以上が主要のメソッドになるのですが、他にも多くメソッドは存在しているため、必要になった場合勉強してここでも追加していこうと考えています!!
そしてこれらの主要メソッドの代替方法としてreact hooks(useEffect)があるのですが、こちらについては別記事で書いております!!
私自身最初にこのサイクルをきちんと理解せずにhooks(useEffect)を使っていたのでそのありがたみを全く理解できていませんでした。
これからもおそらく便利なhooksを利用したいと思いますが、このサイクルとメソッドを構造的に理解していないとreactの深い理解はできませんのでライフサイクルについてはもっともっと理解を深めて行きたいと思います!!参照
- 投稿日:2020-08-12T19:04:37+09:00
ReactでCanvasを使う
ReactでCanvas APIを使う場合どういうタイミングで初期化したらいいかとか
どう状態を持ったらいいか慣れないと結構悩みどころなのでざっくりシンプルなサンプルで実装app.jsimport React,{useState,useEffect} from "react" import ReactDOM from "react-dom" const App = () => { // contextを状態として持つ const [context,setContext] = useState(null) // 画像読み込み完了トリガー const [loaded,setLoaded] = useState(false) // コンポーネントの初期化完了後コンポーネント状態にコンテキストを登録 useEffect(()=>{ const canvas = document.getElementById("canvas") const canvasContext = canvas.getContext("2d") setContext(canvasContext) },[]) // 状態にコンテキストが登録されたらそれに対して操作できる useEffect(()=>{ if(context!==null) { const img = new Image() img.src = "img.jpg" // 描画する画像など img.onload = () => { context.drawImage(img,0,0) // 更にこれに続いて何か処理をしたい場合 setLoaded(true) } } },[context]) useEffect(()=>{ if(loaded) { // それに続く処理 } },[loaded]) return <canvas width="1280" height="720" id="canvas"></canvas> } ReactDOM.render(<App />, document.getElementById('root'))のようにHooksのトリガーで順番に制御していけばタイミング問題でそれほど悩まずにReactでもCanvasが使えるはず
- 投稿日:2020-08-12T17:17:06+09:00
【30分以内で】新しいReact Hooksをnpmパッケージとして公開した話
npmパッケージの作成と公開/更新に便利(なはずの)boilerplateを作ったので
以前作ったuseStackRefをnpmパッケージとして公開してみる最終結果
https://github.com/nariakiiwatani/react-use-stack-ref
https://www.npmjs.com/package/react-use-stack-refこの記事にしたがって進めていく
https://qiita.com/nariakiiwatani/items/ac8acb1822ead4dfc429ダウンロード
dev-npm-package-react-typescript-boilerplateをZIPでダウンロードして適当なフォルダに解凍する。
package.json
を編集package.json(編集部分のみ記載)"name": "react-use-stack-ref", "author": "nariakiiwatani <nariakiiwatani@annolab.com> (https://github.com/nariakiiwatani)", "repository": { "type": "git", "url": "git+https://github.com/nariakiiwatani/react-use-stack-ref" },開発環境のセットアップ
あとで
yarn run setup:demo
をやることになるのでここは単にyarn run setup
でも良かったyarn run setup:all yarn run v1.22.4 $ yarn run setup && yarn setup:demo $ yarn install [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... [4/4] ? Building fresh packages... success Saved lockfile. $ cd demo && yarn install warning package.json: No license field warning No license field [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... [4/4] ? Building fresh packages... ✨ Done in 15.39s.
src
フォルダ内を編集
ModuleFile.ts
削除useStackRef.ts
を作成index.js
とindex.d.ts
を修正index.js(index.d.tsも同じ)import { useStackRef } from './dist/useStackRef' export { useStackRef }ビルドが通るか確認
yarn run build yarn run v1.22.4 $ tsc ✨ Done in 1.74s.
dist
フォルダにビルド結果が出力されていることを確認コミットしておく
ここで、編集を始めるより前に初期状態をコミットしておいた方が良かったと後悔する
git init git add . git commit -m "initial commit" [master (root-commit) 5ab56bf] initial commit including new module 'useStackRef' 19 files changed, 8973 insertions(+) create mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 demo/.gitignore create mode 100644 demo/dist/index.html create mode 100644 demo/package.json create mode 100644 demo/src/components/App.tsx create mode 100644 demo/src/index.tsx create mode 100644 demo/tsconfig.json create mode 100644 demo/webpack.config.js create mode 100644 demo/yarn.lock create mode 100644 index.d.ts create mode 100644 index.js create mode 100644 package.json create mode 100644 src/useStackRef.ts create mode 100644 tsconfig.json create mode 100644 yarn.lockデモページのセットアップ
demo/package.json(変更箇所のみ記載)"dependencies": { "react-use-stack-ref": "link:../" },demo/src/components/App.tsx(変更箇所のみ記載)import { useStackRef } from 'react-use-stack-ref'yarn run setup:demo yarn run v1.22.4 $ cd demo && yarn install warning package.json: No license field warning No license field [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... [4/4] ? Building fresh packages... success Saved lockfile. ✨ Done in 9.92s.デモページのビルドを確認
yarn run build:demo yarn run v1.22.4 $ cd demo && yarn run build warning package.json: No license field $ webpack Hash: 60733be7b91f6c1ca0c1 Version: webpack 4.44.1 Time: 2151ms Built at: 2020-08-12 16:28:08 Asset Size Chunks Chunk Names bundle.js 947 KiB main [emitted] main bundle.js.map 1.08 MiB main [emitted] [dev] main Entrypoint main = bundle.js bundle.js.map [0] multi ./src/index.tsx 28 bytes {main} [built] [./src/index.tsx] 179 bytes {main} [built] + 12 hidden modules ✨ Done in 3.35s.
demo/dist
フォルダに結果が出力される開発用サーバーでデモページが動作することを確認する
yarn run dev yarn run v1.22.4 $ yarn run build $ tsc $ cd demo && yarn run start warning package.json: No license field $ webpack-dev-server --open ℹ 「wds」: Project is running at http://localhost:8080/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from ./dist ℹ 「wdm」: wait until bundle finished: / ℹ 「wdm」: Hash: e51f0a781a90c74c170f Version: webpack 4.44.1 Time: 2518ms Built at: 2020-08-12 16:29:27 Asset Size Chunks Chunk Names bundle.js 1.26 MiB main [emitted] main bundle.js.map 1.46 MiB main [emitted] [dev] main Entrypoint main = bundle.js bundle.js.map [0] multi (webpack)-dev-server/client?http://localhost:8080 ./src/index.tsx 40 bytes {main} [built] [../node_modules/react-dom/cjs/react-dom.development.js] 839 KiB {main} [built] [../node_modules/react-dom/index.js] 1.33 KiB {main} [built] [../node_modules/react/index.js] 190 bytes {main} [built] [./node_modules/webpack-dev-server/client/index.js?http://localhost:8080] (webpack)-dev-server/client?http://localhost:8080 4.29 KiB {main} [built] [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built] [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built] [./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.91 KiB {main} [built] [./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built] [./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.59 KiB {main} [built] [./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built] [./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built] [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built] [./src/components/App.tsx] 135 bytes {main} [built] [./src/index.tsx] 179 bytes {main} [built] + 31 hidden modules ℹ 「wdm」: Compiled successfully.空っぽのページがブラウザで表示される。コンソールにエラーもなし。
デモページを作成
demo/src/component/App.tsximport React, { useState, useCallback, ChangeEvent } from 'react' import { useStackRef } from 'react-use-stack-ref' const App = (props: {}) => { const stack = useStackRef() const [input, setInput] = useState("") const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setInput(e.target.value) } const handlePush = useCallback(e => { stack.push(input) setInput("") }, [stack]) const handlePop = useCallback(e => { const value = stack.pop() value && setInput(value as string) }, [stack]) return (<> <h1>live demo for useStackRef</h1> <input type="text" value={input} onChange={handleChange} /> <button onClick={handlePush} disabled={input.length === 0}>push</button> <button onClick={handlePop} disabled={stack.size === 0} >pop</button> <div>current value: {stack.value}</div> <div>stack depth: {stack.size}</div> </>) } export default App;動作確認
コミットしておく
git add . git commit -m "create demo" [master 5d27f8f] create demo 3 files changed, 34 insertions(+), 14 deletions(-) rewrite demo/src/components/App.tsx (65%)GitHubリポジトリを設定
react-use-stack-refという名前のリポジトリを作成
npmのサイトでnpmトークンを
Read and Publish
の権限で作成
クリップボードにコピーしておくGitHubリポジトリの
Settings -> Secrets
に新たな変数を追加
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
の部分はコピーしたトークン文字列追加後の画面
README.md
を編集してコミットgit add . git commit -m "update readme" [master 5c9508f] update readme 1 file changed, 37 insertions(+), 95 deletions(-) rewrite README.md (99%)
git push
git remote add origin git@github.com:nariakiiwatani/react-use-stack-ref.git git push -u origin master Enumerating objects: 38, done. Counting objects: 100% (38/38), done. Delta compression using up to 8 threads Compressing objects: 100% (31/31), done. Writing objects: 100% (38/38), 148.32 KiB | 898.00 KiB/s, done. Total 38 (delta 5), reused 0 (delta 0) remote: Resolving deltas: 100% (5/5), done. To github.com:nariakiiwatani/react-use-stack-ref.git * [new branch] master -> master Branch 'master' set up to track remote branch 'master' from 'origin'.GitHub Pagesの設定
Publish demo page
というGitHub Actionが自動で実行されて、デモページのビルド結果がgh-pages
ブランチのpushされる。
するとリポジトリのSettings -> Options -> GitHub Pages
でgh-pages
が選べるようになるので選択して、ディレクトリは/(root)
のままでSave
ボタンを押す動作確認
公開用URL( https://nariakiiwatani.github.io/react-use-stack-ref/ )でデモページが動いていることを確認
デモのページをREADMEに追記しておく
[live demo](https://nariakiiwatani.github.io/react-use-stack-ref)
git add . git commit -m "add live demo url in readme" [master f156883] add live demo url in readme 1 file changed, 2 insertions(+)git push Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 361 bytes | 361.00 KiB/s, done. Total 3 (delta 2), reused 0 (delta 0) remote: Resolving deltas: 100% (2/2), completed with 2 local objects. To github.com:nariakiiwatani/react-use-stack-ref.git 5c9508f..f156883 master -> masternpmのサイトに行ってパッケージが公開されていることを確認
...ない!
GitHub Actionのログを確認
semactic-releaseが新しいバージョンがないと判断してpublishしなかったらしい
[8:02:52 AM] [semantic-release] › ✔ Completed step "analyzeCommits" of plugin "@semantic-release/commit-analyzer" [8:02:52 AM] [semantic-release] › ℹ There are no relevant changes, so no new version is released.semantic-releaseのために何かコミットしてpush
READMEにnpmパッケージページへのリンクを記載した。
コミットメッセージを"fix:"で始めるのを忘れない。git add . git commit -m "fix: add npm site in readme" [master fbb5d05] fix: add npm site in readme 1 file changed, 2 insertions(+)git push Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 336 bytes | 336.00 KiB/s, done. Total 3 (delta 2), reused 0 (delta 0) remote: Resolving deltas: 100% (2/2), completed with 2 local objects. To github.com:nariakiiwatani/react-use-stack-ref.git f156883..fbb5d05 master -> master公開確認!
終わりに
ということで、
semantic-release
の仕様を忘れていた以外は特につまづくことなく公開できました。
今回はデモページの作成やこの記事のための記録があってトータルで30〜40分くらいかかりましたが、新しいパッケージの開発環境構築と公開に限って言えば余裕で30分を切るのではないでしょうか。みなさんもどうぞ自分のライブラリを公開する際は使ってみてください。
- 投稿日:2020-08-12T16:28:01+09:00
【備忘録】日本一わかりやすいReact-Redux講座 実践編 #9 「ネイティブアプリ風ヘッダーを作ろう」
はじめに
概要
この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。
前回の講座では、Products の 詳細情報ページを作りました。
今回の講座では、Material-UI の
<AppBar/>
コンポーネントを利用して、ネイティブアプリ風のヘッダーを作成します。※ 前回記事: https://qiita.com/ddpmntcpbr/items/1b1acf97a62cd5101884
動画URL
ネイティブアプリ風ヘッダーを作ろう【日本一わかりやすいReact-Redux講座 実践編#9】
要点
- Material-UIの
<AppBar>
コンポーネントで、ヘッダーを作成できる。- Material-UIの
<Badge>
コンポーネントで、アイコンの肩に数字を表示できる。完成系イメージ
http://localhost:3000
任意のページ上部にヘッダーが表示され、アプリ名ロゴと、各メニューアイコンが表示されています。
メニューアイコンはまだダミーの状態で、クリックしても何も起きません。また、カート右肩のバッジ数も、ダミーの値を表示させています。
メニューアイコンは、ログイン状態のときのみ表示させるため、サインイン画面では表示されません。
http://localhost:3000/signin
メイン
ヘッダーを作ろう
今回はとにかくMaterial-UIを用いて、ビューファイルをゴリゴリ書いていくだけです。
ヘッダーはアプリ内の任意のページ上部で表示をさせるため、
App.jsx
を親コンポーネントとして定義していきます。
src/components/
直下に、新たにHeader
ディレクトリを作り、
- ヘッダー全体のコンポーネントとして
Header.jsx
- ヘッダー右側のメニューアイコン群を表示するコンポーネントして
HeaderMenu.jsx
を作成します。
実装ファイル(ヘッダー)1.src/components/Header/Header.jsx 2.src/components/Header/HeaderMenus.jsx 3.src/components/Header/index.js 4.src/App.jsx1.src/components/Header/Header.jsximport React from 'react'; import {makeStyles} from '@material-ui/core/styles'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import logo from "../../assets/img/icons/logo.png"; import { useDispatch, useSelector } from 'react-redux'; import { getSignedIn } from '../../reducks/users/selectors'; import {push} from "connected-react-router" import {HeaderMenus} from "./index" const useStyles = makeStyles({ root: { flexGrow: 1, }, menuBar: { backgroundColor: "#fff", color: "#444", }, toolBar: { margin: "0 auto", maxWidth: 1024, width: "100%" }, iconButtons: { margin: "0 0 0 auto" } }) const Header = () => { const classes = useStyles(); const dispatch = useDispatch(); const selector = useSelector((state)=>state) const isSignedIn = getSignedIn(selector) return ( <div className={classes.root}> <AppBar position="fixed" className={classes.menuBar}> <Toolbar className={classes.toolBar}> <img src={logo} alt="Torahack Logo" width="128px" onClick={()=>dispatch(push("/"))} /> {isSignedIn && ( <div className={classes.iconButtons}> <HeaderMenus /> </div> )} </Toolbar> </AppBar> </div> ) } export default Header
<AppBar>
の中に<Toolbar>
を記述することで、ヘッダーを実装できます。- メニューアイコンを表示する
<HeaderMenus>
要素はログイン状態でのみ表示させたいので、{isSignedIn && ...
で条件分岐しています。2.src/components/Header/HeaderMenus.jsximport React from "react"; import IconButton from "@material-ui/core/IconButton"; import Badge from "@material-ui/core/Badge"; import ShoppingCartIcon from "@material-ui/icons/ShoppingCart"; import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder"; import MenuIcon from "@material-ui/icons/Menu" const HeaderMenu = () => { return ( <> <IconButton> <Badge badgeContent={3} color="secondary"> <ShoppingCartIcon /> </Badge> </IconButton> <IconButton> <FavoriteBorderIcon /> </IconButton> <IconButton> <MenuIcon /> </IconButton> </> ) } export default HeaderMenu
- return文の中身は1つのHTML要素である必要があるので、
<>...</>
で全体を囲っています。<Badge>
コンポーネントで Icon をラッピングすることで、Icon の右肩に数値を表示させることができます。現在は、badgeContent={3}
としてダミーの数値を表示させていますが、最終的にはここに「カートに入れた商品数」が入ります。3.src/components/Header/index.jsexport {default as Header} from "./Header" export {default as HeaderMenus} from "./HeaderMenus"
- エントリーポイントに追加します。
4.src/App.jsximport React from 'react' import Router from './Router' import "./assets/reset.css" import "./assets/style.css" import {Header} from './components/Header' const App = () => { return ( <> <Header /> <main className="c-main"> <Router /> </main> </> ) } export default App;
- App.jsxで Header を読み込み、全ページで表示させるようにします。
動作確認
「完成系イメージ」と同様のため割愛します。
さいごに
今回の要点をおさらいすると、
- Material-UIの
<AppBar>
コンポーネントで、ヘッダーを作成できる。- Material-UIの
<Badge>
コンポーネントで、アイコンの肩に数字を表示できる。以上です!次回の講座で
<MenuIcon />
の中身を作っていく予定です。このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。
- 投稿日:2020-08-12T15:38:47+09:00
開発中のnpmパッケージをローカルで参照する際に気をつけること
npmパッケージを開発中、デモページも同じリポジトリで開発してたら思わぬところでハマったので経緯と解決法を記録しておく。
経緯
ReactでシンプルなJSONエディタがほしくて作って
Reactでシンプル(by Plain Text)なJSON Editorを作ったデモ。
— 岩谷成晃 (@nariakiiwatani) August 10, 2020
入力された文字列をそのページ自体のCSS Propertiesとして使用している。https://t.co/lLXlXGmCzOhttps://t.co/pYLba2zmTS#react #react-hooks #jsoneditor #json #npm pic.twitter.com/mOIawqyiTi次回別のコンポーネントを作って公開する際にやることを最小化できるように、boilerplateとして切り出してまとめて
git pushでnpmパッケージとデモページを更新する(React&TypeScript用のboilerplate付き) https://t.co/mJrzV9ssxA #Qiita
— 岩谷成晃 (@nariakiiwatani) August 11, 2020せっかくなのでreact-plain-json-editorもこのboilerplateにしたがって編集しなおしたところ、デモページが動かなくなったので
live demoが死んでる
— 岩谷成晃 (@nariakiiwatani) August 11, 2020修正した!
修正した!
— 岩谷成晃 (@nariakiiwatani) August 12, 2020現象
デモページを開くとブラウザで下記のエラーが出現した。
(非常に丁寧なエラーメッセージですね)Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem. at resolveDispatcher (react.development.js:1465) at Object.useState (react.development.js:1496) at ../dist/PlainJsonEditor.js.exports.PlainJsonEditor (PlainJsonEditor.js:38) at renderWithHooks (react-dom.development.js:14803) at mountIndeterminateComponent (react-dom.development.js:17482) at beginWork (react-dom.development.js:18596) at HTMLUnknownElement.callCallback (react-dom.development.js:188) at Object.invokeGuardedCallbackDev (react-dom.development.js:237) at invokeGuardedCallback (react-dom.development.js:292) at beginWork$1 (react-dom.development.js:23203)よくある原因として挙げてくれている3点のうち、1,2は大丈夫そうだったので、3が怪しい。
3. You might have more than one copy of React in the same app
(意訳)Reactが複数含まれているかもしれません
ざっくり現象の概要をまとめてしまうと、「ライブラリ本体とデモページとで別々のReactを参照しているので不整合が起きている」ということになるかと思う。
とりあえず結論だけ知りたい方はこちらへ。
ちなみに、現象としてはこちらのページに書かれているものとおそらく同じだが、少し違う対処をした部分もあるのでこちらはこちらで記しておく。
Reactコンポーネントをnpmパッケージとして開発する状況の整理
- ライブラリ本体の開発
- 開発フォルダをルートディレクトリ(
/
)と呼ぶことにする- ビルドツールは
tsc
- ビルドに関連するファイルは
package.json
- デモページの開発
- 開発フォルダは
demo
ディレクトリ- ビルドツールは
webpack
- ビルドに関連するファイルは
demo/package.json
とdemo/webpack.config.js
問題と対処
問題1: ライブラリ本体のdependenciesにreactが入っている
これはそもそもよくない。普通は
- 動作に必要なパッケージ =>
dependencies
- 開発に必要なパッケージ =>
devDependencies
だが、パッケージとして公開することが前提であれば、使用側との競合を避けるため
- 動作に必要なパッケージ =>
peerDependencies
- 開発に必要なパッケージ =>
devDependencies
とするべきだ。
ただし、これだけではpeerDependencies
はyarn install
(またはnpm install
)に無視されるので、開発時にビルドできなくなる。問題1への対処
- 動作に必要なパッケージ =>
peerDependencies
とdevDependencies
の両方- 開発に必要なパッケージ =>
devDependencies
とするのが良さそうだ。
問題2:
node_modules
とdemo/node_modules
の両方にreact
が含まれている問題1に対処しても、まだ
react
の競合は避けられていないので、
どちらかだけに含むようにする(またはどちらかだけを参照するようにする)必要がある。修正前
demo/package.json(修正前/一部抜粋)"dependencies": { "react": "^16.13.1" }/demo/src/component/App.tsx(修正前/一部抜粋)// ルートディレクトリを直接参照 import { PlainJsonEditor } from '../../../'demo/webpack.config.js(修正前/一部抜粋)// 実際にはこれは書いていなかったが、説明のためにデフォルト値で記載。 resolve: { modules: ['node_modules'] }これらの指定により、デモページ側では
demo/node_modules
にあるreact
が参照されてしまう。修正方針
実際にパッケージが使用される環境を想定すると、
react
はライブラリ側ではなくdemo側にインストールされる方が正しくはあるが、開発環境が不便になるので、ライブラリ側だけにインストールし、demo側からルートのnode_modules
を参照できるようにする。問題2への対処
yarn link
またはnpm link
を使っておいてから"react-plain-json-editor": "latest"
などを指定しても良いが、誰か別の人が使うときに管理する状態を極力増やさないようにしたいので今回は"link:../"
を使った。demo/package.json(修正後/一部抜粋)"dependencies": { "react-plain-json-editor": "link:../" }demo/src/components/App.tsx(修正後/一部抜粋)// dependenciesでlinkしたので、通常のパッケージをimportする時と同じ記法で使用可能 import { PlainJsonEditor } from 'react-plain-json-editor'demo/webpack.config.js(修正後/一部抜粋)resolve: { // ルート側を優先して参照するようにする modules: ['../node_modules', 'node_modules'] }ちなみにこの
demo/webpack.config.json
の修正後も、demo/node_modules
にreact
が含まれていると同様のエラーが起きた。
ここはwebpackの仕様の話になるのだろうが、これ以上は調べていない。問題3:
package.json
にバージョン指定がない※今回の問題とは直接の関係はない
パッケージのリリースはGitHub Actionsに登録した
semantic-release
に任せており、バージョン番号はsemantic-release
が管理するので、package.json
にバージョンを書くと冗長だったり公開バージョンとの間に見た目上の不整合が生じたりすると思って、version
の指定自体を削除していた。するとdemoの
yarn install
時に下記エラーが発生した。error Can't add "react-plain-json-editor": invalid package version "".問題3への対処
package.json
に"version": "0.0.0"
を追加
以上で問題なくデモページが動くようになった。
成果物はこちらです。
react-plain-json-editor
dev-npm-package-react-typescript-boilerplateまとめ
react
(及びライブラリ本体が他に依存するパッケージ)はpackege.json
のpeerDependencies
とdevDependencies
の両方に記載し、dependencies
には記載しない。
peerDependencies
: このパッケージをinstallする際に必要に応じてWarningを出すdevDependencies
: 開発時のためにnode_modules
にインストールされる
demo/node_modules
にreact
を含めない
demo/package.json
のdependencies
にreact
を書かないデモページのビルド時にルートの
node_modules
を参照するようにする
demo/webpack.config.js
のresolve.modules
に['../node_modules', 'node_modules']
を指定する
../node_modules
: ルートディレクトリ側のnode_modules
のことnode_modules
: デモページ側のdemo/node_modules
のことデモページから開発中のライブラリを使用できるようにする
demo/package.json
のdependencies
に"react-plain-json-editor": "link:../"
を記載- または
yarn link
を設定後"react-plain-json-editor": "latest"
を記載ex)yarn/linkを使う方法# まずはルートディレクトリで yarn link # このパッケージをlinkコマンドで参照可能にする(`~/.config/yarn/link`にシンボリックリンクが貼られる) cd demo # demoへ移動 yarn link react-plain-json-editor # linkで登録したパッケージをこのプロジェクト(demo)で使えるようにする参考にしたページ
Reactコンポーネントをnpmパッケージとして開発する
Yarnでローカルのパッケージをaddする方法
npm linkの基本的な使い方まとめ
0000-link-dependency-type.md
依存関係の種類
Webpackを使う時は、resolve.modulesの設定に気を付けよう。(特にModule not found: Error: Can't resolve~が表示された時)
Resolve
- 投稿日:2020-08-12T14:47:04+09:00
jestとreact-testing-libraryでreactのテストをする
はじめに
1回目のReactを使用してWeb画面を作成するでは、reactの簡単なサンプルと共に説明をまとめました。
2回目のReduxとReduxToolkitを使用してReact内でデータを管理するでは、reduxのの簡単なサンプルと共に説明をまとめました。
3回目のReactのRedux内でaxiosを使用した通信をするでは、axiosを使用してREST APIから情報を取得するサンプルと説明をまとめました。
今回は、jestとreact-testing-libraryで仮想DOMのテストをしてみます。環境
- node.js: v12.18.2
- webpack: 4.44.1
- React: 16.13.1
- Redux: 7.2.1
- axios: 0.19.2
- jest: 26.3.0
- react-testing-library: 10.4.8
環境作成
今までの続きとなります。
環境構築などはそちらを見てください。jest
テストをするために、その枠組みを提供するjestというライブラリを使用します。
jestのインストール
yarn add --dev babel-jest react-test-renderer npm install -g jestreact-testing-library
reactが生成する仮想DOMの操作をするためにreact-testing-libraryというライブラリを使用します。
react-testing-libraryのインストール
npm install --save-dev @testing-library/reactaxios-mock-adapter
axiosを使用して通信を行っていますが、テストのたびにREST APIのサーバを起動するのは手間なのでモックを使用してaxiosの処理を置き換えます。
axios-mock-adapterのインストール
npm install --save-dev axios-mock-adapterテストソースを作成する
テストソースを入れるフォルダの作成
テスト用ソースを入れるフォルダとしてルートフォルダの直下にtestフォルダを作成します。
project_root ├─dist // ビルド後のファイルを格納 ├─public // htmlを格納 ├─src // reactのJavaScriptファイルやCSSファイルを格納 ├─test // reactのテストファイルを格納テストファイルの作成
jestは×××.spec.jsや×××.test.jsというファイルを読み込んで実行するため、それに沿ったファイル名にしてください。
Message.spec.jsimport React from "react"; import { render, cleanup, fireEvent } from '@testing-library/react'; import MockAdapter from "axios-mock-adapter"; import axios from 'axios'; import { Message } from "../src/Message"; import { Provider } from 'react-redux' import store from '../store/store' const mockAxios = new MockAdapter(axios); mockAxios.onGet("http://localhost:3000/mess_api").reply(200, [{ id: 1, message: "mock hello" }], ); // テスト実行後にDOMをunmount, cleanupします afterEach(cleanup) describe("Messageテスト", () => { it("初期値:メッセージが表示される", () => { const messageRender = render(<Provider store={store}><Message /></Provider>); // メッセージが2つ(テキストとボタンにあるか) expect(messageRender.getAllByText("メッセージ")).toHaveLength(2); // こんにちはが1つ(ボタンにあるか) expect(messageRender.getAllByText("こんにちは")).toHaveLength(1); // 通信が1つ(ボタンにあるか) expect(messageRender.getAllByText("通信")).toHaveLength(1); // ボタンが2つあるか) expect(messageRender.getAllByRole("button")).toHaveLength(3); }); it("メッセージを押すと入力したものが表示される", () => { const messageRender = render(<Provider store={store}><Message /></Provider>); // こんにちはが1つ(ボタンにあるか) const inputElement = messageRender.getByRole("textbox"); inputElement.innerHTML = "テスト"; fireEvent.change(inputElement); // こんにちはが1つ(ボタンにあるか) const messageElement = messageRender.getAllByText("メッセージ"); // 通信が1つ(ボタンにあるか) fireEvent.click(messageElement[0]); // 通信が1つ(ボタンにあるか) expect(messageRender.getAllByText("テスト")).toHaveLength(1); }); test("通信のテスト", async () => { const messageRender = render(<Provider store={store}><Message /></Provider>); // 通信ボタンを取得 const messageElement = messageRender.getAllByText("通信"); // 通信ボタンをクリック。失敗すればエラーになる await fireEvent.click(messageElement[0]); }); });jestの形式
jestは指定されたフォーマットにしたがって書くことによってテストの実行をしてくれます。
Message.spec.jsインポート // afterEach内の関数はテスト後に実行される afterEach(cleanup) // テストの塊 describe("Messageテスト", () => { // テスト it("初期値:メッセージが表示される", () => { ~~~ テストの内容 ~~~ }); });jestのフォーマットはこのような形になります。他にもテスト前やテストスイート前後にする処理を登録できます。
仮想DOMの生成
仮想DOMの生成には
react-testing-libraryのrender
を使用します。この中に、テストしたいアプリのソースを登録します。
今回はMessageコンポーネントのテストをしたいのでrenderに入れていますが、storeも使っているためそれも適用しています。Message.spec.jsimport React from "react"; import { render, cleanup, fireEvent } from '@testing-library/react'; import { Message } from "../src/Message"; import { Provider } from 'react-redux' import store from '../store/store' ~~~ 省略 ~~~ // 仮想DOMの生成 const messageRender = render(<Provider store={store}><Message /></Provider>); });仮想DOMの操作
生成した仮想DOMに対して
getByXXXやfindByXXX
といった関数を実行すると仮想DOM内の要素を抜き出すことができます。要素のイベントを発火させるためにはfireEvent.change(DOMElement);
で発火させます。
今回は入力するためにgetByRole("textbox")
で抜き出して、値を入れた後にfireEvent.change(inputElement);
で反映させています。
さらにボタンを押すためにgetAllByText("メッセージ")
で抜き出した後に、メッセージという文字列が2つあり最初の要素がボタンであるため、そっちのイベントを発火させます。Message.spec.js~~~ 省略 ~~~ // 入力するテキストボックスを取得 const inputElement = messageRender.getByRole("textbox"); // テキストの入力と反映。 inputElement.innerHTML = "テスト"; fireEvent.change(inputElement); // ボタンの取得 const messageElement = messageRender.getAllByText("メッセージ"); // ボタンのクリック fireEvent.click(messageElement[0]); // ボタンを押した結果を確認 expect(messageRender.getAllByText("テスト")).toHaveLength(1); ~~~ 省略 ~~~axiosのモックを生成
モックを使用するときは、
MockAdapter
を使用して、モックを生成した後に、モック内の処理を記載します。
今回はgetのリクエストを置き換えるのでonGet(URL).reply(httpステータス,レスポンスボディ)
を使用することにより、モック内の処理を定義します。Message.spec.js~~~ 省略 ~~~ import MockAdapter from "axios-mock-adapter"; import axios from 'axios'; // mockの生成 const mockAxios = new MockAdapter(axios); // mock内容を定義 mockAxios.onGet("http://localhost:3000/mess_api").reply(200, [{ id: 1, message: "mock hello" }], ); ~~~ 省略 ~~~テストソースの実行
プロジェクトルートでjestコマンドを実行することでテストできます。
jest終わりに
厳密にテストする場合、action内のテストやstateのテストといった細かい単位でテストすることが可能になります。
それによってテスト結果がNGになった場合に、原因となる場所が限定されるので調査がしやすくなったり、修正後のテストの範囲を限定できたりと様々なメリットがあります。
しかし、そこら辺のテストをちゃんと書くにはReactのことを理解する必要がある上に時間がかかってしまうため、
Reactへのなじみのない人が実施すると時間だけがかかってしまい、テストソースがバグだらけとなったりしてしまいます。
厳密にテストするか大きな枠でテストするかは理解度と時間、テストの影響のトレードオフかなと思っています。
- 投稿日:2020-08-12T14:44:35+09:00
【備忘録】日本一わかりやすいReact-Redux講座 実践編 #8 「Swiperで画像スライダーを作ろう」
はじめに
この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。
前回まで講座で、Products の CRUD 機能を一通り実装しました。
今回の講座では、
-React-swiper
を用いた商品情報詳細ページ(CRUD の R)
-MuiThemeProvider
による Material-UI のテーマカラー設定について実装します。
※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #7 「商品を一覧表示しよう」
要点
<MuiThemeProvider>
で、Material-UI のテーマカラーを設定できる。react-swiper
で画像スワイプ機能を実装できる。- Material-UIの
<Table>
コンポーネントでテーブルを作成できる。完成系イメージ
http://localhost:3000/product/jJV3RsxCgFllZeo771wR
商品情報の詳細ページを作成します。
左側が画像スライダー、右側がサイズ表示用のテーブルになっています。
サイズ表示用テーブルは、Material-UIの
<Table>
コンポーネントを使うことで比較的容易に作成できます。画像スライダーは、
react-id-swiper
というnpmパッケージをインストールすることで、非常に簡単に実装することができます。メイン
MuiThemeProvider によるテーマカラー設定
Material-UI において、アプリ内で使用するテーマカラーを設定する機能として
MuiThemeProvider
というものがあります。これを用いることで、例えばコンポーネント内のフォント一つ一つにカラーコードを定義するような必要がなくなります。
また、「primaryが赤、secondaryが青」のように限られた数の色をテーマカラーを設定して運用するため、無駄な色の使いすぎを自然と防ぐことにも繋がります。
今回は既存のthemeファイルを読み込むだけですが、一連の流れが理解できれば、テーマカラーを簡単に変更することができます。
実装ファイル(theme関連)1.src/assets/theme.js ## themeカラーが定義されたファイル 2.src/index.js ## theme.jsを読み込み、アプリ全体に適用src/assets/theme.jsimport { createMuiTheme } from '@material-ui/core/styles'; // Pick colors on https://material.io/resources/color/#!/ export const theme = createMuiTheme({ palette: { primary: { light: '#88ffff', main: '#4dd0e1', dark: '#009faf', contrastText: '#000', }, secondary: { light: '#ffff81', main: '#ffd54f', dark: '#c8a415', contrastText: '#000', }, grey: { 50: "#fafafa", 100: "#f5f5f5", 200: "#eeeeee", 300: "#e0e0e0", 400: "#bdbdbd", 500: "#9e9e9e", 600: "#757575", 700: "#616161", 800: "#424242", 900: "#212121", A100: "#d5d5d5", A200: "#aaaaaa", A400: "#303030", A700: "#616161" } }, });themeカラーをカラーコードとして定義します。これを、アプリ全体に適用させます。
src/index.jsimport React from 'react'; . . . import {MuiThemeProvider} from "@material-ui/core"; import {theme} from "./assets/theme" . . . ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <MuiThemeProvider theme={theme}> <App /> </MuiThemeProvider> </ConnectedRouter> </Provider>, document.getElementById('root') ); . . .
<MuiThemeProvider>
で<App />
を丸ごとラッピングして、設定完了です。これにより、各コンポーネントの
makeStyle()
内において、任意のコンポーネントファイルconst useStyles = makeStyles((theme) => ({ root: { color: theme.palette.primary.main } . . . }));
theme.js
で定義した色を適用することができます。今回の
theme.js
では、primaryが水色系、secondaryが黄色系で配色されています。詳細ページの大枠作成
商品情報詳細ページの大枠を作成します。
ひとまずは、URLに含まれるid情報を元に正しいproductsをCloud Firstoreから取り出すところまで進めます。テンプレート、及びパスは、
- テンプレート:
ProductDetail.jsx
- path:
/product/:id
として実装します。
実装ファイル(詳細ページ大枠)1.src/Router.jsx 2.src/templates/ProductDetail.jsx 3.src/templates/index.js1.src/Router.jsximport React from 'react'; import {Route, Switch} from "react-router"; import {ProductDetail,ProductEdit,ProductList,Reset,SignIn,SignUp} from "./templates"; import Auth from "./Auth" const Router = () => { return ( <Switch> <Route exact path={"/signup"} component={SignUp} /> <Route exact path={"/signin"} component={SignIn} /> <Route exact path={"/signin/reset"} component={Reset} /> <Auth> <Route exact path={"(/)?"} component={ProductList} /> <Route exact path={"/product/:id"} component={ProductDetail} /> {/*追記*/} <Route path={"/product/edit(/:id)?"} component={ProductEdit} /> </Auth> </Switch> ); }; export default Router2.src/templates/ProductDetail.jsximport React,{ useState,useEffect,useCallback } from "react"; import {useSelector} from "react-redux" import {db} from "../firebase" import { makeStyles } from "@material-ui/core"; import HTMLReactParser from "html-react-parser" const useStyles = makeStyles((theme)=>({ sliderBox: { [theme.breakpoints.down("sm")]: { margin: "0 auto 24px auto", height: 320, width: 320 }, [theme.breakpoints.up("sm")]:{ margin: "0 auto", height: 400, width: 400 } }, detail: { [theme.breakpoints.down("sm")]: { margin: "0 auto 16px auto", height: "auto", width: 320 }, [theme.breakpoints.up("sm")]:{ margin: "0 auto", height: "auth", width: 400 } }, price: { fontSize: 36 } })); const returnCodeToBr = (text) => { if (text === "") { return text } else { return HTMLReactParser(text.replace(/\r?\n/g, '<br/>')) } }; const ProductDetail = () => { const classes = useStyles(); const selector = useSelector((state)=>state); const path = selector.router.location.pathname; const id = path.split("/product/")[1]; const [product,setProduct] = useState(); useEffect(()=>{ db.collection("products").doc(id).get() .then(doc => { const data = doc.data(); setProduct(data) }) },[]); return ( <section className="c-sention-wrapin"> {product && ( <div className="p-grid__row" > <div className={classes.sliderBox}> </div> <div className={classes.detail}> <h2 className="u-text__headline">{product.name}</h2> <p className={classes.price}>{product.price.toLocaleString()}</p> <div className="module-spacer--small"></div> <div className="module-spacer--small"></div> <p>{returnCodeToBr(product.description)}</p> </div> </div> )} </section> ) }; export default ProductDetail
[theme.breakpoints.down("sm")]: {...}
とすることで、smサイズの幅をブレイクポイントとして、適用するスタイルを変更しています。useEffect()
を用いて、初期レンダー時に、DBから該当するidの情報を取得します。returnCodeToBr()
で、改行コード\n
を、改行のHTMLタグ<br />
に変換しています。この関数に用いているhtml-react-parser
は、この時点ではインストールしていないはずなので、npm からインストールします。html-react-parserのインストール$ npm install --save html-react-parser3.src/templates/index.jsexport {default as Home} from './Home' export {default as ProductDetail} from './ProductDetail' export {default as ProductEdit} from './ProductEdit' export {default as ProductList} from './ProductList' export {default as Reset} from './Reset' export {default as SignIn} from './SignIn' export {default as SignUp} from './SignUp'いったん動作確認をします。すでに
ProductCard.jsx
において、リストアイテムをクリックすることで/product/:id
へ遷移されるよう定義されているため、そこからアクセスします。
http://localhost:3000
スカートをクリックすると、
http://localhost:3000/product/ewzuLXPK2Dr0owCG3dt7
スカートの商品情報が取得できていることが分かります!
画像スライダーの設置
この
ProductDetail
テンプレートに対して、画像スライダーを設置します。画像スライダーの実装には、
react-id-swiper
というnpmパッケージを利用します。依存関係にあるswiper
と合わせて、インストールしておきます。swiperとreact-id-swiperをインストール$ npm install -S swiper@5.4.2 react-id-swiper@3.0.0画像スライダー用のコンポーネントして
ImageSwiper.jsx
を定義し、それをProductDetail.jsx
で読み込みます。実装ファイル(画像スライダー)1.src/components/Products/ImageSwiper.jsx 2.src/components/Products/index.js 3.src/templates/ProductDetail.jsx1.src/components/Products/ImageSwiper.jsximport React, { useState } from "react"; import Swiper from "react-id-swiper"; import NoImage from "../../assets/img/src/no_image.png" import 'swiper/css/swiper.css' const ImageSwiper = (props) => { const [params] = useState({ pagination: { el: ".swiper-pagination", type: "bullets", clickable: true, dynamicBullets: true }, navigation: { nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev" }, loop: true }) const images = props.images return ( <Swiper {...params}> {images.length === 0 ? ( <div className="p-media__thumb"> <img src={NoImage} alt="no images"/> </div> ) : ( images.map(image => ( <div className="p-media__thumb" key={image.id}> <img src={image.path} alt="商品情報"/> </div> )) )} </Swiper> ); }; export default ImageSwiper;
react-id-swiper
は、1.
import 'swiper/css/swiper.css'
でスライダー用のcssを読み込む。
1. スライダーの性質をparams
として定義
2.<Swiper {...params}>
としてJSXに展開という流れで実装をします。
'swiper/css/swiper.css'
は、npmインストールすることによって、node_modules
下に保存されるcssファイルです。この中で、スライダーとしての動的な挙動が実装されているイメージです。また、
params
の中で定義するパラメータを変えることで、様々な形式のスライダーを実装できるようです(公式リファレンス参照)export {default as ImageArea} from "./ImageArea" export {default as ImagePreview} from "./ImagePreview" export {default as ImageSwiper} from "./ImageSwiper" //追記 export {default as ProductCard} from "./ProductCard" export {default as SetSizeArea} from "./SetSizeArea"3.src/templates/ProductDetail.jsx. . . import {ImageSwiper} from "../components/Products" {*追記*} . . . const ProductDetail = () => { . . . return ( <section className="c-sention-wrapin"> {product && ( <div className="p-grid__row" > <div className={classes.sliderBox}> <ImageSwiper images={product.images} /> {*追記*} </div> <div className={classes.detail}> <h2 className="u-text__headline">{product.name}</h2> <p className={classes.price}>{product.price.toLocaleString()}</p> <div className="module-spacer--small"></div> <div className="module-spacer--small"></div> <p>{returnCodeToBr(product.description)}</p> </div> </div> )} </section> ) }; export default ProductDetail画像スライダー動作確認
http://localhost:3000/product/jJV3RsxCgFllZeo771wR
コード量も少なく簡単に画像スライダーが実装できるのはすばらしいですね!
サイズ表示テーブル
最後に、商品情報のサイズを表示するテーブルを作成します。
<SizeTable>
コンポーネントを作成し、<ProductDetail>
で読み込みます。実装ファイル(サイズ表示テーブル)1.src/components/Products/SizeTable.jsx 2.src/components/Products/index.js 3.src/templates/ProductDetail.jsx1.src/components/Products/SizeTable.jsximport React from 'react'; import Table from "@material-ui/core/Table"; import TableRow from "@material-ui/core/TableRow"; import TableCell from "@material-ui/core/TableCell"; import TableBody from "@material-ui/core/TableBody"; import IconButton from "@material-ui/core/IconButton"; import TableContainer from "@material-ui/core/TableContainer"; import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'; import {makeStyles} from "@material-ui/styles"; const useStyles = makeStyles({ iconCell: { padding: 0, height: 48, width: 48 } }) const SizeTable = (props) => { const classes = useStyles(); const sizes = props.sizes; return ( <TableContainer> <Table> <TableBody> {sizes.length > 0 && ( sizes.map(size => ( <TableRow key={size.size}> <TableCell component="th" scope="row">{size.size}</TableCell> <TableCell>残り{size.quantity}点</TableCell> <TableCell className={classes.iconCell}> {size.quantity > 0 ? ( <IconButton> <ShoppingCartIcon /> </IconButton> ) : ( <div>売切</div> )} </TableCell> <TableCell className={classes.iconCell}> <IconButton> <FavoriteBorderIcon /> </IconButton> </TableCell> </TableRow> )) )} </TableBody> </Table> </TableContainer> ); }; export default SizeTable;Material-UIの
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>
で、テーブルを実装しています。
2.src/components/Products/index.jsexport {default as ImageArea} from "./ImageArea" export {default as ImagePreview} from "./ImagePreview" export {default as ImageSwiper} from "./ImageSwiper" export {default as ProductCard} from "./ProductCard" export {default as SetSizeArea} from "./SetSizeArea" export {default as SizeTable} from "./SizeTable" //追記3.src/templates/ProductDetail.jsx. . . import {SizeTable} from "../components/Products" {*追記*} . . . const ProductDetail = () => { . . . return ( <section className="c-sention-wrapin"> {product && ( <div className="p-grid__row" > <div className={classes.sliderBox}> <ImageSwiper images={product.images} /> </div> <div className={classes.detail}> <h2 className="u-text__headline">{product.name}</h2> <p className={classes.price}>{product.price.toLocaleString()}</p> <div className="module-spacer--small"></div> <SizeTable sizes={product.sizes}/> {*追記*} <div className="module-spacer--small"></div> <p>{returnCodeToBr(product.description)}</p> </div> </div> )} </section> ) }; export default ProductDetail
<SizeTable>
コンポーネントをインポートします。動作確認
http://localhost:3000/product/jJV3RsxCgFllZeo771wR
左に画像スライダー、右にサイズテーブルが設置されています!
さいごに
今回の要点をおさらいすると、
-<MuiThemeProvider>
で、Material-UI のテーマカラーを設定できる。
-react-swiper
で画像スワイプ機能を実装できる。
- Material-UIの<Table>
コンポーネントでテーブルを作成できる。以上です!
このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。
- 投稿日:2020-08-12T12:52:23+09:00
Recoilの簡単な説明
Recoilをつかってみた
Qiita投稿2回目です。
今回はRecoilを軽く利用したのでRecoilについて簡単に説明します。
私が利用したの
recoil 0.0.10
です。Recoilとは
RecoilとはFacebookにより作成されたReact専用の状態管理ライブラリのこと。
状態管理とは
状態とはフロントの保持するデータのこと。
アプリケーションの規模が大きくなるほどコンポーネントでのデータの管理が複雑になるのでデータの状態を管理する必要があり、データを集中管理することを状態管理という。
Reactの場合、Redux Vueの場合はVuexなどが存在する。
状態管理についてfluxアーキテクチャを調べるとより理解が深まると考えています。
Recoilの特徴
簡単
React専用の状態管理ライブラリ
Reduxよりコンパクトな状態管理を行うことが可能
カスタムフックとの相性が良い
現時点(2020/08/12)では正式リリースはされていない
上記がRecoilの簡単な説明です。
Recoilの良い点
カスタムフックとの相性がとても良かったです。
状態管理を容易に行えるので、Reactを最近利用し始めた私でも簡単に利用できました。
Reactの状態管理ライブラリのなかでTypeScriptとの相性が良い。
Recoilについて
Atoms
Selectors
上記2つがRecoilの主機能です。
Atoms
Atomとはデータを管理する場所です。
Atomにはどのコンポーネントからでもアクセスすることが可能で、全てのコンポーネントにデータを渡すことができます。
Selectors
Atomに格納されたデータを取り出す機能です。
データを全て取り出す場合やデータを元のまま取り出す場合はAtomのみでも可能ですがデータを指定して取り出す場合やデータを加工して取り出す場合にはSelectorを利用します。
また、Selectorには非同期の処理を行うことが可能なので大量のデータを処理する場合にはSelectorを利用します。
Selectorはエラーのハンドリングを行うことが可能でAtomからデータを抽出した際に失敗した場合にどう対処するかを指定することもできます。
私は公式ドキュメントをTypeScriptで記述したのですが公式ドキュメントのTodoアプリではAtomのみでもアプリの機能は乏しいですがアプリ開発ができると感じました。
Recoilの利用方法
今回は公式ドキュメントに記述されている内容をTypeScriptで記述し、説明します。
TypeScript App.tsximport React from 'react' import { RecoilRoot } from 'recoil' import { Sample } from './Sample' export function App() { <RecoilRoot> <Sample /> </RecoilRoot> }まずはルートコンポーネントにRecoilRootを記述します。
これによりStateを格コンポーネントに受け渡すことができるようになります。
※ RecoilRootは必ずとしてルートコンポーネントに記述する必要はありません。今回はルートコンポーネントに記述しましたが状態管理をしたいコンポーネント郡の親コンポーネントであれば構いません。
Atoms
atom.tsimport { atom } from 'recoil'; type Sample = { id: number; title: string; content: string; completed: boolean; } const initialSample: Sample[] = []; export const sampleListState = atom({ key: 'sampleListState', default: initialSample, });上記がatomのサンプルコードです。
const initialSample: Sample[] = [];
まずデータを保存する配列を用意します。
export const sampleListState = atom({ key: 'sampleListState', default: initialSample, });ここがatomのメインとなる部分です。
defaultで先ほど記述したデータ格納場所を指定します。
これでatomでデータの保持ができるようになりました。
useRecoilValueとuseSetRecoilState
useRecoilValueについて
const sampleList = useRecoilValue(sampleListState);
上記コードにより、先ほど作成したsampleListStateに格納されたデータがsampleListに移動されました。
useRecoilValueによってatomに存在するデータをコンポーネントに渡すことができるようになります。
const setSampleList = useSetRecoilState(sampleListState);
まずuseSetRecoilValueにsampleListStateにアクセスできるように設定します。
useSetRecoilStateによりatomに格納されているデータに追加することができます。
useRecoilValueとusseSetRecoilValueはReactに搭載されているuseStateの機能にとても似ていると思いました。
Selectors
Atomsでデータの取り出しとデータの格納方法がわかりましたが、条件を指定してデータを取り出したい場合にはAtomsではできません。
そこでSelectorsを利用します。Selectorsを使用することで、条件を指定したデータを取り出すことができます。
type Sample = { id: number; title: string; content: string; completed: boolean; } const initialSample: Sample[] = [];Sampleデータにcompletedが格納されています。completedがtrueの場合とfalseの場合で取り出すデータを変えたい場合は
const sampleListFilterState = atom({ key: 'sampleListFilterState', default: 'Show All', });まずはatomに全てのデータを参照できるようなコードを追加します。
ここからSelectorsの処理です。
selector.tsimport { selector } from 'recoil'; import { sampleListState, sampleListFilterState } from './atom'; type Sample = { id: number; title: string; content: string; completed: boolean; } const filterSampleListState = selector({ key: 'filteredSampleListState', get: ({get}) => { const filter: string = get(sampleListFilterState); const list: Sample[] = get(sampleListState); switch (filter) { case 'Show Completed': return list.filter((item) => item.isComplete); case 'Show Uncompleted': return list.filter((item) => !item.isComplete); default: return list; } }, });これによりfilterSampleListStateが選択された場合は、
Show Completed
が選択された場合は、completedがtrueのデータの取り出しが可能になり、
Show Uncompleted
が選択された場合は、completedがfalseのデータを取り出します。
では次はどちらのデータを取り出すかをコンポーネントで選択できるような処理を追加しましょう
Sample.tsximport React from 'react'; import { useRecoilValue } from 'recoil'; import { filterSampleListState } from './selector.ts' import { SampleFilter } from './SampleItem'; import { SampleItem } from './SampleItem'; export function Sample() { const sampleList = useRecoilValue(filterSampleListState); return( <> <SampleFilter /> {sampleList.map((sampleItem) => ( <SampleItem item={sampleItem} key={sampleItem.id} /> ))} </> ) }上記により、sampleFilterで選択されたデータをSampleItemコンポーネントに渡すように設定されました。
ではデータをどうやって選択するかについて説明します。
SampleFilter.tsximport React from 'react'; import { useRecoilValue } from 'recoil'; import { sampleListFilterState } from './atom'; export function SampleFilter() { const [filters, setFilters] = useRecoilValue(sampleListFilterState); const updateFilter = (event: React.ChangeEvent<{ value: unknown }>) => { setFilter(event.target.value as string); } return ( <select value={filter} onChange={updateFilter}> <option value="Show All">All</option> <option value="Show Completed">Completed</option> <option value="Show Uncompleted">Uncompleted</option> <select/> ) }まずuseRecoilValueにsampleListFilterStateを設定します。
selectで3個の要素を選択することができます。selectのvalueはfilterSampleListStateにて記述したswitchのcaseと一致します。
これで選んだデータがSampleItemコンポーネントにpropsで渡されるようになりました。
以上がSelectorsの簡単な使い方です。
参考サイト
- 投稿日:2020-08-12T11:07:49+09:00
Ant Design で Table の columns の dataIndex の候補を表示する型
Ant Design で Table の columns の dataIndex の候補を表示する型
antd の
Table
のcolumns
には表示する対象を指定するdataIndex
がありますが、
これは手動で指定する必要があります。no-suggestiontype Data = { id: number; name: string; }; export const columns: TableProps<Data>['columns'] = [ { title: 'ID', key: 'id', // これだと候補がでない dataIndex: '', } ];そこで以下のような型を定義することで
dataIndex
の型を指定してみます。columns-typeimport { ColumnGroupType, ColumnType } from 'antd/lib/table/interface'; type AnyData = Record<string, unknown>; type RenderReturn<TRecord = AnyData> = ReturnType< NonNullable<ColumnType<TRecord>['render']> >; type Column<TRecord = AnyData> = | (Omit<ColumnGroupType<TRecord>, 'render'> & { render?: ( value: TRecord, record: TRecord, index: number ) => RenderReturn<TRecord>; }) | (ColumnType<TRecord> & { dataIndex?: keyof TRecord; render?: <T = TRecord>( value: T, record: TRecord, index: number ) => RenderReturn<TRecord>; }); type Columns<TRecord = AnyData> = Column<TRecord>[]; type Data = { id: number; name: string; }; export const columns: Columns<Data> = [ { title: 'ID', key: 'id', dataIndex: '', }, ];定義した
Columns
を指定することでdataIndex
に候補が出るようになりました。
- 投稿日:2020-08-12T07:38:35+09:00
ReactとFirebaseを使ってログインフォームを実装する①
ReactとFirebaseを使用してProgateのようなログインフォームを作成してみます。Google、Twitter、FacebookのソーシャルログインとEメール、パスワードを用いた一般的なログインを実装します。
ソーシャルログインはFirebaseを利用すれば比較的簡単ですが、結構大変でした。。私と同じように個人で開発している人のお役に立てれば幸いです。
React、Redux、react-router、Firebaseの基本的な使い方を理解している方を対象としています。全4回です。
使用するパッケージ
- 状態管理にredux、、react-redux、
- 非同期処理にredux-thunk
- cssフレームワークにmaterial-ui
- ルーティングにreact-router-dom
- 完成品はGitHubにアップしています。
https://github.com/nineharker/react-firebase-login-form
プロジェクト作成
create-react-appで作成します。このままでもいいのですが、ESlintとPrettierを設定した方が便利なので設定しておきます。使うエディタはVS Codeです。自分のエディタがある人はそれを使ってください。
VS CodeでReact、ESlint、Prettierを設定する方法は下記の記事を参照してください。
https://harkerhack.com/vscode-react-eslint-prettier/
GitHubに設定済のプロジェクトがあるのでダウンロードしましょう。
https://github.com/nineharker/react-firebase-login-form#install create-react-app npm -g install create-react-app # clone repo git clone https://github.com/nineharker/react-vscode-eslint-prettier.git cd react-vscode-eslint-prettier yarn install必要なファイルを作成する
まず最低限のページを作成します。必要なパッケージをインストールしましょう。
yarn add @material-ui/core redux react-redux react-router-dom redux-thunkランディングページとログイン後のページの二つを作ります。それと二つのページで共有するヘッダーを用意します。src/componentsディレクトリ作成し、 components以下にLandingPage.jsxとLoginedPage.jsx、NavBar.jsxを作成します。
src/components/LandingPage.jsximport React, { Component } from 'react'; # ランディングページ class LandingPage extends Component { render() { return <div>LandingPage</div>; } } export default LandingPage;src/components/LoginedPage.jsximport React, { Component } from 'react'; # ログイン後のページ class LoginedPage extends Component { render() { return <div>LoginedPage</div>; } } export default LoginedPage;ヘッダーはmateriaru-uiを使います。Reactのコンポーネントとして利用できます。
AppBar参考
https://material-ui.com/components/app-bar/src/components/NavBar.jsximport React, { Component } from 'react'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; const useStyles = makeStyles(theme => ({ root: { flexGrow: 1, }, menuButton: { marginRight: theme.spacing(2), }, title: { flexGrow: 1, }, })); export default function NavBar() { const classes = useStyles(); return ( <AppBar position="static"> <Toolbar> <Typography variant="h6" className={classes.title}> News </Typography> <Button color="inherit">ログイン</Button> </Toolbar> </AppBar> ); }ルーティング
react-router-domを使用して二つのページをルーティングします。App.jsxを編集しましょう。の外にを書くことで、どちらのページでもヘッダーが表示されるようになります。
react-router参考
https://qiita.com/muiyama/items/b4ca1773580317e7112esrc/Appimport React, { Component } from 'react'; import { Router, Route, Switch } from 'react-router-dom'; import history from './history'; import LandingPage from './components/LandingPage'; import LoginedPage from './components/LoginedPage'; import NavBar from './components/NavBar'; class App extends Component { render() { return ( <Router history={history}> <NavBar /> <Switch> <Route path="/" exact component={LandingPage} /> <Route path="/logined" exact component={LoginedPage} /> </Switch> </Router> ); } } export default App;react-router-domのBrowserRouterではなくRouterを使っているので、別に履歴を管理するhistory.jsが必要になります。 src以下に作成しましょう。historyが別にあることによってGoogleアナリティクスの設定などができるようになります。
src/history.jsconst createHistory = require('history').createBrowserHistory; export default createHistory();それではyarn startでローカルサーバーを起動してみましょう。
http://localhost:3000/ に青色のヘッダーとLandingPageという文字、
http://localhost:3000/logined に青色のヘッダーとLoginedPageという文字があればルーティングは完了です!おわり
今回で基本的なページを作成することができました! 全体のページ数はユーザーが最初に訪れるランディングページと、ログイン後に偏移するページの二つです。
次回はログインフォームを作っていきます。普通に作ると面倒ですが、フレームワークを使えば簡単に実装できます。
- 投稿日:2020-08-12T06:58:17+09:00
UikitとFontAwesomeをNext.js(React.js)上で動くようにする
本手順は、Next.js上で、UikitとFontAwesomeを動くようにする手順です。
前提
ツール バージョン ツール バージョン React.js 16.13.1 Next.js 9.4.4 uikit 3.5.5 react-icons 3.10.1 背景
- もともと以下のようなhtmlで、の中でcdnを使用してUikitとFontAwesomeを呼び出していた。
- Next.js (React.js)に移行した途端に、uikitのアイコンとFontAwesomeのアイコンが読み込まれなかった。
<!--UiKit--> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.3.1/css/uikit.min.css" integrity="***" crossorigin="anonymous" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.3.1/js/uikit.min.js" integrity="***" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.3.1/js/uikit-icons.min.js" integrity="***" crossorigin="anonymous"></script> <!--FontAwesome--> <script src="https://kit.fontawesome.com/1d615c07d9.js" crossorigin="anonymous"></script> ... <!--FontAwesome--> <i class="fas fa-user-shield fa-5x"></i> <!--UiKit--> <span uk-icon="icon: users" class="uk-margin-small-right uk-icon"></span>原因
- Reactで読み込むための指定方法は下記のようになるらしい (参考)
html × <span uk-icon="icon: users" class="uk-margin-small-right uk-icon"></span> ○ <span data-uk-icon="icon: users" class="uk-margin-small-right uk-icon"></span>
- そもそも、レンダー時にUikit, FontAwesome共にうまく読み込めていなかった。。 => そもそもCDNを使うべきかは未検討だったため、いったんローカルでインストールする方法を模索。 => npm install を使って必要なものをインストールする方針に変更
解決
- uk-***のところをdata-uk-***に変更
- UiKitの必要なものをインストールし、コードを修正 => uikit
$ npm install uikitimport UIkit from 'uikit'; import Icons from 'uikit/dist/js/uikit-icons'; ... UIkit.use(Icons);
- FontAwesomeの必要なものをインストールし、コードを修正 => react-icons
$ npm install react-iconsimport { IconContext } from "react-icons"; import { FaUserShield , FaGlobe, FaUsers } from 'react-icons/fa'; ... render() { return ( <IconContext.Provider value={{ style: { fontSize: '5em' } }}> <div> <FaUserShield /> <FaGlobe /> <FaUsers /> </div> ); } ...参考
- 投稿日:2020-08-12T00:45:43+09:00
create-react-appでのReact環境構築がつらいので新しい簡易版ツール作った
create-xxx-app
作った。
- ブログ記事: https://sbfl.net/blog/2020/08/11/create-xxx-app/
- npm: https://www.npmjs.com/package/create-xxx-app
npx create-xxx-app プロジェクト名 --template reactでReact+TypeScriptのプロジェクト作られるはず。
インストールされるボイラープレート部分についてはいろいろ考え中なのでまだ中途半端です。後で大幅に変わる可能性があります。
でも一応動くReactのウェブアプリ作れるよ!ちなみにxxxという文字の並びには英語圏ではエロい意味があるらしい。後で知った。
読みたくないから短くまとめろ
- create-react-appつらい
- 縛りきついから入れた瞬間にejectしたくなる
- eject後のファイルが意味不明すぎて解読するより自分で始めから構築した方が早い
- でもcreate-react-app捨てて生きていけない
- 手動構築だるい!create-react-app無しの時代には戻れない!
- だから新しいツール作ったよ
- わりとニッチ向け
- ボイラープレートをコピーしてnpm installするだけ
- create-react-appと手動構築の中間ぐらいの存在
create-react-appつらい問題
create-react-app(CRA)、最初は便利だなーって思うんですけど、だんだんとブラックボックス化された部分に不満が溜まってきます。
あれができないとかこれができないとか、変なところでエラー出るとか、やり方は存在するけど遠回りとか。重要な部分を隠蔽しないでほしい。
たまにならいいけどほぼ毎プロジェクトで不満出る。公式的には不満あるならejectすりゃいいじゃん的な雰囲気を放ってますが、ejectするとwebpackの闇を煮詰めたようなファイルが出力されます。
これを解析するよりも自分で初めから環境構築した方が早い。それに加えて同梱されてる初期ファイルが邪魔。ServiceWorkerいらねえ場面とかCSS Module使いたくない場面とかいっぱいある。カスタムテンプレート作るの面倒だから毎回ちまちま削除してる。
じゃあお前はCRA無しで生きていけんのって言われたら生きていけない。webpackの設定ファイル書きたくないし
npm install なんちゃら かんちゃら @types/ほんにゃら
を打ち込みまくるのもつらい。もう完全にCRAに飼い慣らされてる。カスタムテンプレートという手もあるけど、それも結局はCRAの手の上で踊るだけ。同じ結末になる。
それならお前はどうしたいのって言われたら別にどうもしたくなく、白馬の王子様がなんかうまい感じに助けてくれるの期待してました。
結局何が欲しいんだよ
結局自分は何が欲しいのかよくわからんまま半年ぐらい経過しました。CRAには不満。じゃあどうしてほしいのって聞かれると答えられない。どうしようもねえ。
でもやっぱり
npm install アレ コレ ソレ @types/ドレ
を打ち続ける日々には辟易してましたし、CRA使うと後で死ぬほど苦労するという経験則だけはたまってます。単純なReactアプリにnext使うのはなんか違うという気分もある。snowpackとかも使ってみたいんだけど、create-snowpack-appの公式テンプレートの出来がかなりアレ。変なところから設定をextendsしてるから設定ファイルの中身をサクっと追えない。自分はどこに向かえばいいんだ。
もうcpとnpm installだけでよくないか
自分がReactプロジェクトを作るときによくやってた作業について考えてみると、結局コピーとnpm installしかしてない気がしてきました。tsconfig.jsonをどっかからコピーしてきてちょっといじって、reactと@types/reactと入れて、みたいなのの繰り返し。
じゃあもうここの部分だけ自動化してあとは手動でやれば良くね?
シェルスクリプトだとどこに置くの問題とか発生するし、ボイラープレートリポジトリ作ってもいちいちcloneするのすら面倒。yarn createは好きではないし、じゃあもうここツール化しちゃえばいいんじゃね?って思いつきました。npxで叩きたいからどこからでも使えるようにnpm publishしました。
create-xxx-appの誕生です。
正直なところとしてはcpしてnpm installしてくれればあとは勝手にやるんですよ。jest欲しかったら自分で入れるしeslintの設定欲しかったら自分で書く。とりあえずプロジェクト作る最初のcpとnpm installだけしてくれればいい。一生面倒見てくれる献身的な
react-scripts
とか完全に不要で、コピーとインストールだけしてくればそれでよかった。プロジェクト構築ツールはプロジェクト構築終わったら存在が完全に消えていてほしいという願いです。痕跡を残さないでほしい。cxa-template-react
--template react
で使うテンプレートはこんな感じでpublishしてあります。https://www.npmjs.com/package/cxa-template-react
中身はここ。
https://github.com/kotofurumiya/cxa-template-react
中身見ると(2020/08/12時点では)reactとesbuild入れてbrowser-syncでグルグル回してるだけ。
ブラックボックス化された部分は作られないし、令和にもなってbrowser-sync使ってるのが丸わかりだし、変更入るたびにフルビルドが走るtools/devserver.js
のクソコードがフルオープンになってます。
これだよこういうの求めてたんだよ(自画自賛)。create-xxx-appで作られたプロジェクトは簡単にカスタマイズできます。だってフォルダ作ってnpm installしてるだけだもん。browser-syncいらなかったらuninstallすれば終わるし、prettierほしかったらinstallすれば動く。
カスタムテンプレート
cxa-template-templatename
の命名規則でpublishすればnpx create-xxx-app prjname --template templatename
で取ってこれます。テンプレートパッケージのフォルダ構造とかはcxa-template-reactを見てパクってください。ちなみにReactには依存してないので任意のNode.jsプロジェクトを作れます。
さいごに
CRAは素晴らしいけど、現実で使おうとすると割と結構いろんな問題出てきます。CRAは目的にマッチしないけど手動でやるのは嫌って人向けのツールがcreate-xxx-appです。
「CRAつらいけど手動でやるのはな〜」みたいな人がいたらぜひ使ってみてください。まだエラーハンドリングとかもろくにできてないので環境によっては動かないとかもあるかもですが、よろしくお願いします。
- 投稿日:2020-08-12T00:41:45+09:00
Parsing error: The keyword 'import' is reserved の解決法
解決法
eslintから怒られているので、
.eslintrc
に怒られないようにこちらを追記してください。{ ... "parserOptions": { "ecmaVersion": 7, "sourceType": "module", "ecmaFeatures": { "jsx": true } } ... }参考
https://github.com/yannickcr/eslint-plugin-react/issues/447#issuecomment-208625730eslintのプロパティについてはこちら
https://eslint.org/docs/user-guide/configuring