- 投稿日:2020-05-28T22:43:20+09:00
Reactでchartjsを使ったときにY軸のMAX値の設定ではまった話
はじめに
Reactでchart.jsを使って積み上げ棒グラフ表示したときに、Y軸のMAX値の設定ではまったことがあったので、まとめてみます。
使ったもの
- react(v16.13.1)
- react-chartjs-2(v2.9.0)
- chart.js(v2.9.3)
- lodash(v4.17.15)
作りたかったもの
- 積み上げ棒グラフ
- ボタンクリックで実数表示⇔割合表示を切り替える
- 実数表示のY軸の最大値はauto(kg)にしたい(単位は適当)
- 割合表示のY軸の最大値は100(%)にしたい
イメージはボタンクリックでstateで管理してるフラグを切り替えて、
chart.js
のコンポーネントに渡すdata
とoptions
を変化させるイメージです。プロジェクト作成
create-react-appでReactのプロジェクトを作成して、上記のとおり必要なパッケージをインストールします。(react以外)
create-react-app app npm install --save react-chartjs-2 chart.js lodashデータの準備
とりあえず適当に準備する。
App.jsthis.dataset = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "label1", data: [36.1, 41.2, 50.4, 32.9, 29.4], backgroundColor: "red", }, { label: "label2", data: [51.9, 60.5, 45.6, 55.5, 75.5], backgroundColor: "blue", }, { label: "label3", data: [80.1, 88.2, 75.7, 69.4, 100.2], backgroundColor: "green", }, { label: "label4", data: [120.5, 110.5, 128.9, 130.2, 150.5], backgroundColor: "purple", }, { label: "label5", data: [80.5, 85.9, 77.7, 68.5, 60.9], backgroundColor: "yellow", }, ], };オプションの準備
積み上げ棒グラフのオプションを準備する。
とりあえず、実数表示用に
scales.yAxes[0].scaleLabel.labelString
に実数(kg)を入れておく。
後でレンダー時に割合表示なら書き換える。App.jsthis.options = { scales: { xAxes: [ { stacked: true, display: true, scaleLabel: { display: true, }, }, ], yAxes: [ { stacked: true, display: true, scaleLabel: { display: true, labelString: "実数(kg)", }, ticks: { min: 0, }, }, ], }, maintainAspectRatio: false, };レンダー部分
データとオプションを
Bar
コンポーネントに渡せばグラフが描画できる。
データとオプションは実数表示か割合表示かを変更するように関数を呼んで生成App.jsrender() { const data = this.changeDataSet(); const options = this.changeOptions(); console.log(options); return ( <div> <input type="button" value="変更" onClick={this.onClickButton} /> <Bar data={data} options={options} height={400} /> </div> ); }this.changeDataSet部分
this.state.isProb
で実数表示か割合表示かを切り替える。
lodash
のcloneDeep
で値だけコピーして、そちらを加工する。割合表示時は小数点以下第二位を四捨五入する。
App.jschangeDataSet() { let dataset = _.cloneDeep(this.dataset); // 割合表示の時にデータを変更 let sum = 0; if (this.state.isProb === true) { for (let j = 0; j < dataset.datasets[0].data.length; j++) { sum = 0; for (let i = 0; i < dataset.datasets.length; i++) { sum += dataset.datasets[i].data[j]; } for (let i = 0; i < dataset.datasets.length; i++) { if (sum > 0) { dataset.datasets[i].data[j] = _.round( (dataset.datasets[i].data[j] / sum) * 100, 1 ); } else { dataset.datesets[i].data[j] = 0; } } } } return dataset; }this.changeOptions部分
同じく
this.state.isProb
で実数表示か割合表示かを切り替える。
lodash
のcloneDeep
で値だけコピーして、そちらを加工する。とりあえず
scales.yAxes[0].scaleLabel.labelString
だけ変更する。App.jschangeOptions(data) { let options = _.cloneDeep(this.options); if (this.state.isProb === true) { options.scales.yAxes[0].scaleLabel.labelString = "割合(%)"; } return options; }確認してみる
実数表示時(初期表示時)
scales.yAxes[0].ticks.max
を設定していないので、Y軸の最大値は自動で調整される。割合表示時
scales.yAxes[0].ticks.max
を設定していないので、Y軸の最大値は自動で調整される。しかし四捨五入しているので、全部加算した時に100を超えることがあるので、Y軸の最大値が100ではなく120となってしまう。
なので、割合表示時だけY軸の最大値を100にしたい。
this.changeOptionsに追記
割合表示の時だけ
options.scales.yAxes[0].ticks.max = 100;
を追記して、Y軸の最大値を100にしてみる。App.jschangeOptions(data) { let options = _.cloneDeep(this.options); if (this.state.isProb === true) { options.scales.yAxes[0].scaleLabel.labelString = "割合(%)"; options.scales.yAxes[0].ticks.max = 100; } return options; }再度確認してみる
割合表示時
うまくいった!!Y軸の最大値が100になっている!!
しかし、もう一度変更ボタン押して、実数表示に戻すと・・・
Y軸の最大値が100のまま戻らなくなった・・
Bar
コンポーネントに一度渡したオプションが残るっぽいです。実数表示時にも最大値を入れてみる
実数表示時にも
options.scales.yAxes[0].ticks.max
を書き換えれば問題とおもったのでやろうとしたのですが、
合計値がどれくらいの大きさになるかわからない場合にすべてをif文とかで書くのも厳しいし、そもそもグラフのY軸のメモリもいい感じに自動で調整されるので、そのあたりが無理と気づく・・・で結果どう対応したのか・・・
round
で四捨五入しているところをfloor
で切り捨ててとりあえず対応した・・・
合計値が100を超えないのでY軸の最大値も自動調整のままでいけた。微妙ですが、見た目もそんなにおかしくないような気がするのでまあいいでしょう。(ダメかもしれない・・・)
App.jsdataset.datasets[i].data[j] = _.floor( (dataset.datasets[i].data[j] / sum) * 100, 1 );[コメントより追記]
undefined
を設定すれば、最大値の設定が消えるみたいです。
なので、こちらがベストプラクティスだと思います。App.jsif (this.state.isProb === true) { options.scales.yAxes[0].scaleLabel.labelString = "割合(%)"; options.scales.yAxes[0].ticks.max = 100; } else { options.scales.yAxes[0].ticks.max = undefined; }まとめ
Reactでchart.jsを使うときに、state管理でグラフ描画を切り替えるときにはまった話を書きました。
結局、納得できる解決はできていないので、こうしたらいいんじゃない?とかありましたらコメントお願いします!!解決はできたが、もっとこうしたらいいんじゃない?とかありましたらコメントお願いします!!
ソースはこちらにあります。
- 投稿日:2020-05-28T20:32:02+09:00
日本一わかりやすいReact入門【実践編】#6~9 学習備忘録
概要
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact入門【実践編】』の自分用学習備忘録です。
#1~5までの記事はこちら。
#6...条件分岐とコールバック関数のbind
続いて、チャット部分のコンポーネントを作成します。
- Chats
- Chat
- Chat
- Chat
- ...
AnswersList -> Answer と同じ構造。
これと並行して、Answerコンポーネントをクリックすることで、次の回答に遷移するよう、App.jsxで定義した selectAnswer()関数を、bindによってAnswerコンポーネントへ渡していきます。
修正ファイルは、
- src/App.jsx
- components/Chats.jsx
- components/Chat.jsx
- components/AnswersList.jsx
- components/Answer.jsx
- components/index.jssrc/App.jsximport React from 'react'; import './assets/styles/style.css'; import defaultDataset from "./dataset" import {AnswersList, Chats} from "./components/index" export default class App extends React.Component { constructor(props) { super(props); this.state = { answers: [], chats: [], currentId: "init", dataset: defaultDataset, open: false } this.selectAnswer = this.selectAnswer.bind(this) } displayNextQuestion = (nextQuestionId) => { const chats = this.state.chats console.log(nextQuestionId) chats.push({ text: this.state.dataset[nextQuestionId].question, type: 'question' }) this.setState({ answers: this.state.dataset[nextQuestionId].answers, chats: chats, currentId: nextQuestionId }) } selectAnswer = (selectedAnswer, nextQuestionId) => { switch(true) { case (nextQuestionId === 'init'): this.displayNextQuestion(nextQuestionId) break; default: const chats = this.state.chats; chats.push({ text: selectedAnswer, type: 'answer' }) this.setState({ chats: chats }) this.displayNextQuestion(nextQuestionId) break; } } componentDidMount() { const initAnswer = ""; this.selectAnswer(initAnswer, this.state.currentId) } render() { return ( <section className="c-section"> <div className="c-box"> <Chats chats={this.state.chats}/> <AnswersList answers={this.state.answers} select={this.selectAnswer} /> </div> </section> ); } }ここは実行順序が少し複雑なので、文字で書き起こして整理してみる。
初回render時の挙動
- componentDidMount()により、selectAnswer()に初期の引数が渡される。第1引数は空の文字列、第2引数は'init'
- selectAnswer()において、
case (nextQuestionId === 'init'):
が実行され、this.displayNextQuestion()
に'init'
が渡される。- displayNextQuestion()で、
this.state.dataset['init'].question
が呼び出され、空っぽのchats
に最初のchatが入る(typeはquestion)。- answers対しても、nextQuestionId='init'に対応する値が、datasetから呼び出され代入される。
- currentIdの更新がこのタイミングで入る。今回はcurrentId='init'が代入される。
- これらの
chats
、answers
が、それぞれChatsコンポーネントとAnswersコンポーネントに渡される。チャット欄が更新されていく流れ
- AnswersListコンポーネントに渡しているselectAnswer()関数は、最終的にAnswerコンポーネントに渡り、onClickイベントにより実行されることになる。その際、Answerコンポーネントが保有している
props.content
とprops.nextId
が、selectAnswer(props.content,props.nextId)
の形で渡される- selectAnswer()のswitch分岐のうち、defaultの方が実行される。まず、selectedAnswer引数に渡された
props.content
がchatsに追加される(typeはanswer)。その後、nextQuestionId引数に渡されたprops.nextIdがdisplayNextQuestion()に渡り、answers、currentIdが更新される- 更新された
chats
、answers
が、それぞれChatsコンポーネントとAnswersコンポーネントに渡される。component/Chats.jsximport React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import List from '@material-ui/core/List'; import {Chat} from "./index" const useStyles = makeStyles((theme) => ({ root: { width: '100%', maxWidth: '36ch', backgroundColor: theme.palette.background.paper, }, })); const Chats = (props) => { const classes = useStyles(); return ( <List className={classes.root}> {props.chats.map((chat,index) => { return <Chat text={chat.text} type={chat.type} key={index.toString()} /> })} </List> ); } export default Chats
- Material-UIのList React Componentを一部使用。
- App.jsxよりもらった
props.chats
は、AnswersListコンポーネントと同様に、map関数で繰り返し処理。component/Chat.jsximport React from 'react'; import ListItem from '@material-ui/core/ListItem'; import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import Avatar from '@material-ui/core/Avatar'; import UserIcon from '../assets/img/no-profile.png' import BotIcon from '../assets/img/ai_computer_sousa_robot.png' const Chat = (props) => { const isQuestion = (props.type === 'question'); const classes = isQuestion ? 'p-chat__row' : 'p-chat__reverse'; return ( <ListItem className={classes}> <ListItemAvatar> {isQuestion ? ( <Avatar alt="icon" src={BotIcon} /> ) : ( <Avatar alt="icon" src={UserIcon} /> )} </ListItemAvatar> <div className="p-chat__bubble">{props.text}</div> </ListItem> ) } export default Chat
- Material-UIのList React Componentを一部使用。
- chatの種類(type)が「question」か「answer」かで条件分岐を行っている。
component/AnswersList.jsximport React from 'react' import {Answer} from './index' const AnswersList = (props) => { return ( <div className="c-grid__answer"> {props.answers.map((value, index) => { return <Answer content={value.content} nextId={value.nextId} key={index.toString()} select={props.select} /> })} </div> ) } export default AnswersList
- 新たにnextIdとselect(selectAnswer関数のこと)を、Answerコンポーネントに渡している
component/Answer.jsximport React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; const useStyles = makeStyles((theme) => ({ root: { }, })); const Answer = (props) => { return ( <Button variant="contained" color="primary" onClick={() => props.select(props.content,props.nextId)} > {props.content} </Button> ) } export default Answer
onClick
により、select(=selectAnswer関数)が実行される。
components/index.jsexport {default as AnswersList} from './AnswersList' export {default as Answer} from './Answer' export {default as Chats} from './Chats' export {default as Chat} from './Chat'
- コンポーネントが増え次第、ここに追記する。
上手く行けば、質問をクリックするたびチャット欄が更新され、また新たな質問が表示されます。
ここまでで、チャットボットとしての最低限の機能は実装できました!
#7...Material-UIのスタイルをカスタマイズしよう
Hook APIを用いて、Material-UIのスタイルを変更する。まずはHook APIをインストールする。
terminal$ npm install --save @material-ui/stylesAnswerコンポーネント、Chatsコンポーネントにstyleを定義します。
components/Answers.jsximport React from 'react'; import { makeStyles, createStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; const useStyles = makeStyles(() => ( createStyles({ "button": { borderColor: '#FFB549', color: '#FFB549', fontWeight: 600, marginBottom: '8px', "&:hover": { backgroundColor: '#FFB549', color: '#fff' } } }) )); const Answer = (props) => { const classes = useStyles(); return ( <Button className={classes.button} variant="outlined" onClick={() => props.select(props.content,props.nextId)} > {props.content} </Button> ) } export default Answer
- @material-ui/core/stylesから
createStyles
をimportする。- Material-UIでは、この
createStyles()
の中にstyleを定義するconst classes = useStyles();
で定義したclassを、styleを適用させたいタグに入れる
components/Chats.jsximport React from 'react'; import { makeStyles, createStyles } from '@material-ui/core/styles'; import List from '@material-ui/core/List'; import {Chat} from "./index" const useStyles = makeStyles(() => ( createStyles({ "chats": { height: 400, padding: '0', overflow: 'auto' } }) )); const Chats = (props) => { const classes = useStyles(); return ( <List className={classes.chats}> {props.chats.map((chat,index) => { return <Chat text={chat.text} type={chat.type} key={index.toString()} /> })} </List> ); } export default Chats
overflow: 'auto'
により、<List>
タグの子要素がheight:400
を超えると、スクロールバーが表示されるstyleが上手く当てられていると、
- 選択肢ボタンについて、通常時は白背景の黄色文字、マウスホバー時は黄色背景の白文字に。
- チャットを何度か繰り返すと、スクロールバーが表示される
のようになっているはず。#8...ライフサイクルの活用
自動スクロール機能
チャットが更新されたときに、スクロールが自動で下まで移動する仕様を実装する。ライフサイクルメソッドのうち、componentDidUpdate()を用いる。
components/Chats.jsx... <List className={classes.chats} id={"scroll-area"} > ...スクロールをさせる
<List>
タグにidを持たせる。component/App.jsx... componentDidUpdate(){ const scrollArea = document.getElementById("scroll-area") if (scrollArea) { scrollArea.scrollTop = scrollArea.scrollHeight } } ...Chatsコンポーネントの値が更新されるたびにcomponentDidUpdate()が実行され、自動スクロールが実行される。
ディレイ機能
チャットボット側の返信を意図的に遅らせる。setTimeout()関数を使用する。
src/App.jsx... selectAnswer = (selectedAnswer, nextQuestionId) => { switch(true) { case (nextQuestionId === 'init'): // this.displayNextQuestion(nextQuestionId) setTimeout(() => this.displayNextQuestion(nextQuestionId), 500); break; default: const chats = this.state.chats; chats.push({ text: selectedAnswer, type: 'answer' }) this.setState({ chats: chats }) // this.displayNextQuestion(nextQuestionId) setTimeout(() => this.displayNextQuestion(nextQuestionId), 1000); break; } } ...displayNextQuestion()の実行にディレイをかける。setTimeoutの単位はms(ミリセカンド)。・
外部サイトへの転送
selectedAnswer()に渡されたnextQuestionIdが
http:~
から始まるURL文字列の時、datasetから値を取り出すのではなく、該当するURLへページを転送させるように実装します。src/App.jsxselectAnswer = (selectedAnswer, nextQuestionId) => { switch(true) { case (nextQuestionId === 'init'): setTimeout(() => this.displayNextQuestion(nextQuestionId), 500); break; // ここから追記 case (/^https:*/.test(nextQuestionId)): const a = document.createElement('a'); a.href = nextQuestionId; a.target = '_blank'; a.click(); break; // ここまで追記 default: ... } }
- 正規表現の
/^https:*/
を用いて、nextQuestionIdの先頭文字列がhttp:~
で始めるかどうかをチェック<a>
タグを生成し、クリック処理をすることで転送させるここまでで「問い合わせる」(nextQuestionId=‘contact’)以外の選択肢については、エラーを出さなくなりました
#9...問い合わせ用モーダルとSlack通知を実装しよう
問い合わせ用モーダル
FormDialogコンポーネントと、TextInputコンポーネントを新たに作ります。
FormDialogコンポーネントが問合せモーダル自体の表し、その中に入力スペースの部分のみを、TextInputコンポーネントが担当するイメージです。
新規作成or修正ファイルは以下の通り。
- src/App.jsx
- component/Form/FormDialog.jsx
- component/Form/TextInput/jsx
- component/index.js
src/App.jsximport React from 'react'; import './assets/styles/style.css'; import defaultDataset from "./dataset" // FormDialogコンポーネントを追加 import {AnswersList, Chats, FormDialog} from "./components/index" export default class App extends React.Component { constructor(props) { super(props); this.state = { answers: [], chats: [], currentId: "init", dataset: defaultDataset, open: false } this.selectAnswer = this.selectAnswer.bind(this) // FormDialogコンポーネントに渡す関数をバインド this.handleClickOpen = this.handleClickOpen.bind(this) this.handleClose = this.handleClose.bind(this) } displayNextQuestion = (nextQuestionId) => { const chats = this.state.chats chats.push({ text: this.state.dataset[nextQuestionId].question, type: 'question' }) this.setState({ answers: this.state.dataset[nextQuestionId].answers, chats: chats, currentId: nextQuestionId }) } selectAnswer = (selectedAnswer, nextQuestionId) => { switch(true) { case (nextQuestionId === 'init'): setTimeout(() => this.displayNextQuestion(nextQuestionId), 500); break; // ユーザーが'問合せたい'をクリックした時に、this.handleClickOpen()が実行される case (nextQuestionId === 'contact'): this.handleClickOpen() break; case (/^https:*/.test(nextQuestionId)): const a = document.createElement('a'); a.href = nextQuestionId; a.target = '_blank'; a.click(); break; default: const chats = this.state.chats; chats.push({ text: selectedAnswer, type: 'answer' }) this.setState({ chats: chats }) setTimeout(() => this.displayNextQuestion(nextQuestionId), 1000); break; } } // openの値でモーダルの開閉を制御 handleClickOpen = () => { this.setState({ open: true }); }; // openの値でモーダルの開閉を制御 handleClose = () => { this.setState({ open: false }); }; componentDidMount() { const initAnswer = ""; this.selectAnswer(initAnswer, this.state.currentId) } componentDidUpdate(){ const scrollArea = document.getElementById("scroll-area") if (scrollArea) { scrollArea.scrollTop = scrollArea.scrollHeight } } render() { return ( <section className="c-section"> <div className="c-box"> <Chats chats={this.state.chats}/> <AnswersList answers={this.state.answers} select={this.selectAnswer} /> // openの値と、openの値を変更する関数を渡す <FormDialog open={this.state.open} handleClose={this.handleClose} /> </div> </section> ); } }
- 問合せモーダルを開くタイミングは、ユーザーが「問い合わせる」の選択肢を選んだときのみなので、
handleClickOpen
関数はApp.jsx
のみで使用する- 一方、問合せモーダルを閉じるタイミングは、①情報が送信されたとき、と②問合せモーダル内の「キャンセル」を押したときであり、②を実装するためには、
handleClose
関数をFormDialogコンポーネントに渡しておく必要がある。
components/Forms/FormDialog.jsximport React from 'react' import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import {TextInput} from "../index" export default class FormDialog extends React.Component { constructor(props) { super(props); this.state = { name: "", email: "", description: "" } this.inputName = this.inputName.bind(this) this.inputEmail = this.inputEmail.bind(this) this.inputDescription = this.inputDescription.bind(this) } inputName = (event) => { this.setState({ name: event.target.value }) } inputEmail = (event) => { this.setState({ email: event.target.value }) } inputDescription = (event) => { this.setState({ description: event.target.value }) } render() { return ( <Dialog open={this.props.open} onClose={this.props.handleClose} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > <DialogTitle id="alert-dialog-title">{"問い合せフォーム"}</DialogTitle> <DialogContent> <TextInput label={"お名前"} multiline={false} rows={1} value={this.state.name} type={"text"} onChange={this.inputName} /> <TextInput label={"メールアドレス"} multiline={false} rows={1} value={this.state.email} type={"email"} onChange={this.inputEmail} /> <TextInput label={"お問い合わせ内容"} multiline={true} rows={5} value={this.state.description} type={"text"} onChange={this.inputDescription} /> </DialogContent> <DialogActions> <Button onClick={this.props.handleClose} color="primary"> キャンセル </Button> <Button onClick={this.props.handleClose} color="primary" autoFocus> 送信する </Button> </DialogActions> </Dialog> ) } }
- reactでは、inputで送信する値に関しても、stateとして定義・管理する必要がある。
- inputName()、inputEmail()、inputDescription()を定義して、テキストフィールドに入力された内容を逐次stateとして保存する処理を行う。
- この時点では、「送信する」ボタンを押しても、情報は送信されずに問合せモーダルが閉じる処理だけが行われる
components/Forms/TextInput.jsximport React from 'react'; import TextField from '@material-ui/core/TextField'; const TextInput = (props) => { console.log(props.multiline) console.log(props.rows) return ( <TextField fullWidth={true} label={props.label} margin={"dense"} multiline={props.multiline} rows={props.rows} value={props.value} type={props.type} onChange={props.onChange} /> ) } export default TextInput
- Material-UIの『Text Field』を使用。
components/index.jsexport {default as AnswersList} from './AnswersList' export {default as Answer} from './Answer' export {default as Chats} from './Chats' export {default as Chat} from './Chat' export {default as FormDialog} from './Forms/FormDialog' export {default as TextInput} from './Forms/TextInput'
- FormDialigコンポーネント、TextInputコンポーネントを作成したので、追記。
Slack通知機能
モーダルに入力された内容を、Slackまで通知させます。まず、Slack側の設定を行い、WebhookURLを取得します。流れとしては、
- (未作成なら)自分一人だけのワークスペースを新たに作る
- 作成したワークスペースに対して、Slackアプリを作成する
- Incoming Webhooksを有効にし、WebhookURLを取得する
詳細は『slackのIncoming webhookが新しくなっていたのでまとめてみた』というQiita記事が参考になりました。
WebhookURLを取得できたら、FormDialog.jsxに、Slackに通知させる関数を定義します。
components/Forms/FormDialog.jsx... submitForm = () => { const name = this.state.name const email = this.state.email const description = this.state.description const payload = { text: 'お問い合わせがありました\n' + 'お名前: ' + name + '\n' + 'Email: ' + email + '\n' + 'お問い合わせ内容:\n' + description } const url = 'ここにWebhookURLを記述' fetch(url, { method: 'POST', body: JSON.stringify(payload) }).then(() => { alert('送信が完了しました!しばらくお待ちください') this.setState({ name: "", email: "", description: "" }) return this.props.handleClose() }) } ... // <Button onClick={this.props.handleClose} color="primary" autoFocus> <Button onClick={this.submitForm} color="primary" autoFocus> 送信する </Button> ...「送信する」ボタンのonClickイベントに、定義したsubmitForm()関数を設定します。
お問い合わせフォームに適切な値を入力し送信すると、、、
無事、通知が届きました!
Slackへの通知は初実装でしたが、意外と簡単にできました!
記事が長くなったのでいったんここまで!続きはこちら。
参考URL
- 投稿日:2020-05-28T16:45:10+09:00
function式とarrow式の違いをサッカー選手を呼び出しながら説明するよ
概要
function式とarrow式の違いをサッカー選手を使いながら軽く説明するよ
問題
早速、問題を出します。
aとbの違いわかりますか?const a = function() { console.log('neymar') } const b = () => { console.log('neymar') }この場合だとbのほうが合計文字数がすくなくなったくらいですかね。
次
// let, const であればトップレベルで宣言してもwindowオブジェクトのプロパティを生成しませんが今回は問題の都合上varでいきます // よほどのことがない限りこんな書き方をしないけどグローバルのnameに代入するために記載 // var宣言が怖い理由の一つもこれですね。 var name = '??global-messhi??' const printName = function() { console.log('printName', this.name) } const arrowPrintName = () => { console.log('arrowPrintName', this.name) } const obj1 = { name: 'nagatomo', printName } const obj2 = { name: 'king-kazu', arrowPrintName } // 出力される名前はなんでしょう obj1.printName() obj2.arrowPrintName()
答え
obj1.printName() // nagatomo obj2.arrowPrintName() // ??global-messhi??
アロー関数式とfunction式の違い
先程の問題の結果を見て、文字数が短くなっただけではないことはすでにわかったと思います。
this
の指している場所が違います。通常のfunction式の中でthisを使うと、その呼び出し元のオブジェクトを指します。
var name = '??global-messhi??' const printName = function() { console.log('printName', this.name) } const obj1 = { name: 'nagatomo', printName } const obj2 = { name: 'king-kazu', printName } obj1.printName() // 'nagatomo' obj2.printName() // 'king-kazu'なのでこの場合printName関数内で使っている
this.name
の値は呼び出し元がobj1とobj2なのかで値が異なってきます。
function式だとthisが呼ばれたタイミングで決定されちゃんと自立できていない人のような振る舞いです。
なのでfunction式は直感でわかりにくいので苦労します。アロー関数は結論から言うと
this
は宣言された時点で、this
を確定します。var name = '??global-messhi??' const arrowPrintName = () => { console.log('arrowPrintName', this.name) // この時点でのthisはグローバルになる } const obj1 = { name: 'nagatomo', arrowPrintName } const obj2 = { name: 'king-kazu', arrowPrintName } obj1.arrowPrintName() // '??global-messhi??' obj2.arrowPrintName() // '??global-messhi??'なので、呼び出し元がobj1だろうが、obj2だろうが、関係なくて直感的にthisの指している場所がわかります。
今学んだことを再確認するために、もっかい問題出します。
class Person { constructor(name) { this.name = name } printName = function () {console.log('printName', this.name)} arrowPrintName = () => {console.log('arrowPrintName', this.name)} } const printName = function() { console.log('printName', this.name) } const arrowPrintName = () => { console.log('arrowPrintName', this.name) } class Person2 { constructor(name) { this.name = name } printName = printName arrowPrintName = arrowPrintName } const kakitani = new Person('kakitani') const maezono = new Person2('maezono') // 出力される名前はなんでしょう kakitani.printName() kakitani.arrowPrintName() maezono.printName() maezono.arrowPrintName()
答え
kakitani.printName() // kakitani kakitani.arrowPrintName() // kakitani maezono.printName() // maezono maezono.arrowPrintName() // ??global-messhi??
次
const bindPrintName = function() { console.log(this.name) }.bind(this) // function式をarrow関数と同じ感じでthisをつかえるようにするための魔法のコトバだよ class Person3 { constructor(name) { this.name = name } bindPrintName = bindPrintName } // 出力される名前はなんでしょう const zaccheroni = new Person3('zaccheroni') zaccheroni.bindPrintName()
答え
const zaccheroni = new Person3('zaccheroni') zaccheroni.bindPrintName() // ??global-messhi??
Reactでよくあるパターン
これだとボタンを押した時
Uncaught TypeError: Cannot read property 'state' of undefined
が出ます。
理由わかりますか?export default class Hoge extends Component { constructor(props) { super(props); this.state = { name: 'zico' }; } handlePrintName() { const { name } = this.state; return name } render() { const { name } = this.state; return ( <button onClick={this.handlePrintName}> print name </button> ); } }解決方法
handlePrintName()
をアロー関数にする// 変更前 handlePrintName() { // これでthisが定まっていない const { name } = this.state; return name } // 変更後 handlePrintName = () => { // これでthisをHogeにbind const { name } = this.state; return name }ただReactの場合これには問題点があります。
レンダー内でアロー関数を利用するとコンポーネントがレンダーされるたびに新しい関数が作成されるため、子コンポーネントでReact.memoやPureComponentを使ってた場合に正しく比較されなくなる
ClassComponentでは
bind(this)
を使う場合が多いexport default class Hoge extends Component { constructor(props) { super(props); this.state = { name: 'zico' }; this.handlePrintName = this.handlePrintName.bind(this) } handlePrintName() { const { name } = this.state; return name } render() { const { name } = this.state; return ( <button onClick={this.handlePrintName}> print name </button> ); } }参考記事
JavaScriptの「this」は「4種類」??
JavaScript の this を理解する多分一番分かりやすい説明
- 投稿日:2020-05-28T16:42:29+09:00
[メモ] react-scriptsで環境毎にビルドできる様にする
概要
Create React Appで書いたフロントエンドのソースを環境毎に値を変更してビルドしたかったので対応しました。
やり方
環境変数を使います。
詳しくはこちらを参照してください。
Adding Custom Environment Variables簡単に要点をまとめると
.env
ファイル内に環境変数を定義しておけばJavaScript内でprocess.env.XXXXX
で取得可能
REACT_APP_
というプリフィックスにしておけばpublic/index.html
内でも使用可能yarn start
時には.env.development
系、yarn build
時には.env.production
系の環境変数が使用される
- この辺りのルールは上記のページを参照する
複数の環境変数を切り替えられないか調べましたが特に情報が見つかりませんでした。
npmに何かありそうな気配でしたが、あまり依存するのもどうかと思ったので簡単な仕組みで実現することにしました。
ビルド時は.env.production.local
に書かれている環境変数が最も優先されて使用されるため、このファイルをビルド時に入れ替えれば要件は満たせます。
簡易なshellスクリプトを作成しました。set-env-file-for-react-script.sh
単に
.env.production.xxxx
という環境毎の.envファイルを作成しておくと、それをコピーするだけのshellです。
このファイルとpackage.json
のスクリプトを組み合わせて以下の様なコマンドを用意しました。"build:develop": "./set-env-file-for-react-script.sh develop && yarn build && rm ./.env.production.local"これで
build:develop
を実行すると.env.production.develop
を.env.production.local
に入れ替えてビルドするという事が実現できます。
最低限ですがこれでやりたい事は一旦実現できました。よりよいやり方や標準的な手法があれば教えて下さい。
- 投稿日:2020-05-28T14:23:00+09:00
[React]create-react-appを使わずに環境構築 ~設定ファイル編~
はじめに
この記事では、以前書いた[React][mac]create-react-appを使わずに環境構築で作成した設定ファイルの記述について、簡単に説明していきます。
.babelrc
// Babelに関する設定 { // Presentsというプラグインのプリセット "presets": ["@babel/preset-env", "@babel/preset-react"] }.eslintrc.json
.eslintrc.json{ "parser": "babel-eslint", // ESLintがされる環境の指定 "env": { // ブラウザーでES6を使用する "browser": true, "es6": true }, // パーサーの設定 "parserOptions": { // ES6のmodulesを使用する "sourceType": "module", // JS言語の追加機能の指定 "ecmaFeatures": { // 分割代入を有効にする "experimentalObjectRestSpread": true, // JSXを有効にする "jsx": true } }, // ルールのデフォルトを指定 "extends": ["eslint:recommended", "plugin:react/recommended"], // React用のプラグインを指定 "plugins": ["react"], // ルールの設定(ルールのデフォルトと違う設定や、デフォルトにない項目を追加などをする) "rules": { // console.logを使えるようにする "no-console": "off" }, "settings": { "react": { "version": "detect" } } }webpack.config.js
webpack.config.jsmodule.exports = { // アプリ起動時に動作すべきJSのソースファイルを指定(複数のファイルを指定可能) entry: { app: "./src/index.js" }, // 出力されるファイル名、パスを指定 output: { // 絶対パスで指定する必要があるためカレントディレクトリを表す __dirname と連結している path: __dirname + '/public/js', // [name] と書くと上記 entry で付けたキー(app)が使われる filename: "[name].js" }, // webpack-dev-serverの設定 devServer: { // webで公開するディレクトリー(index.htmlがある)のパス contentBase: __dirname + '/public', // 開発サーバーが使用するポート番号 port: 8080, // webpackが出力するJSのあるディレクトリーが contentBase と異なる場合に指定 publicPath: '/js/' }, // デバックできるようにmapファイルを作成する指示 devtool: "eval-source-map", mode: 'development', // ファイルの種類ごとにloaderを指定 // test: に書かれている正規表現とマッチする場合、そのオブジェクトに書かれた loader が動く module: { rules: [{ // ESLint の loader を実行 test: /\.js$/, enforce: "pre", exclude: /node_modules/, loader: "eslint-loader" }, { // css-loader , style-loader を実行 test: /\.css$/, loader: ["style-loader","css-loader"] }, { // Babel の loader を実行 test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }] } };おわり
最後まで見て頂き、ありがとうございました。
- 投稿日:2020-05-28T11:17:08+09:00
[React][mac]create-react-appを使わずに環境構築
はじめに
Reactを学習するために教材を購入したのですが、その開発環境の構築に時間を要したため忘備録として残したいと思います。
この記事では正常にインストール出来たかを確認するため画面にHello Worldと表示させるまでを行います。
create-react-appは使用しません。また、途中作成する各設定ファイルについての記事を[React]create-react-appを使わずに環境構築 ~設定ファイル編~で書きました。
Node.jsのインストール(Homebrew)
// node.jsがインストールされているか確認(バージョンが表示されればインストールされている) $ node -v // バージョンが表示されなければ以下でインストール $ brew update $ brew install nodejsnpmコマンドのインストール
npmコマンドはNode.jsと一緒にインストールされていますが、バージョンが古いとアップデートの必要があります。
// バージョンを確認 $ npm -v // 必要に応じてアップデート $ npm install npm --globalプロジェクトの作成
プロジェクト用ディレクトリーを作成し、その中にパッケージの管理ファイル(package.json)を作成します。
今回は、hello_world
という名前でディレクトリーを作成します。// プロジェクト用ディレクトリーを作成し移動 $ mkdir hello_world $ cd hello_world // パッケージの管理ファイル(package.json)を作成 $ npm init -yパッケージの管理ファイル(package.json)の編集
以下のように編集します。
package.json{ "name": "hello_world", "version": "1.0.0", "description": "Hello React", "private": true, "main": "index.js", "scripts": { "start": "webpack-dev-server", "webpack": "webpack -d" }, "keywords": [], "author": "", "license": "ISC" }
"description"
と"scripts"
の内容を編集しました。
description
・・・説明文private
・・・github等に置かないプライベートなプロジェクトの指定start
・・・開発ツールの起動スクリプトwebpack
・・・webpack実行様スクリプトnpmパッケージのインストール
以下のコマンドで必要なパッケージをインストールしていきます。
途中でnpm ERR
と表示された場合はエラーが発生していますので、タイプミスなどを確認してみたり、必要に応じて調べてみてください。
$ npm install react react-dom
$ npm install webpack webpack-cli webpack-dev-server --save-dev
$ npm install @babel/core @babel/preset-env @babel/preset-react @babel/cli --save-dev
$ npm install eslint babel-eslint eslint-loader eslint-plugin-react --save-dev
$ npm install css-loader style-loader babel-loader --save-dev
インストール結果の確認用のコードを書くためのディレクトリーを作成
$ mkdir src $ mkdir public設定ファイルの作成
一部のファイル名は
.
から始まりますが、これらのファイルはファインダーには表示がされませんので注意してください。
以下のファイルを作成します。
No. 作成ファイル 1 .babelrc 2 .eslintrc.json 3 webpack.config.js 4 public/index.html 5 src/index.js 設定ファイルの編集
先ほど作成したファイルを編集していきます。
.babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-react"] }.eslintrc.json
{ "parser": "babel-eslint", "env": { "browser": true, "es6": true }, "parserOptions": { "sourceType": "module", "ecmaFeatures": { "experimentalObjectRestSpread": true, "jsx": true } }, "extends": ["eslint:recommended", "plugin:react/recommended"], "plugins": ["react"], "rules": { "no-console": "off" }, "settings": { "react": { "version": "detect" } } }webpack.config.js
module.exports = { entry: { app: "./src/index.js" }, output: { path: __dirname + '/public/js', filename: "[name].js" }, devServer: { contentBase: __dirname + '/public', port: 8080, publicPath: '/js/' }, devtool: "eval-source-map", mode: 'development', module: { rules: [{ test: /\.js$/, enforce: "pre", exclude: /node_modules/, loader: "eslint-loader" }, { test: /\.css$/, loader: ["style-loader","css-loader"] }, { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }] } };表示用ファイルの編集
public/index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta http-equiv="X-UA-Compatible" content="IE=Edge, chrome=1" /> <title>Hello world.</title> </head> <body> <div id="root"></div> <script type="text/javascript" src="js/app.js" charset="utf-8"></script> </body> </html>src/index.jsimport React from 'react' import ReactDOM from 'react-dom' ReactDOM.render( <h1>Hello, world!!</h1>, document.getElementById('root') )以上で準備は整いました。
インストール結果の確認
では、webpack開発サーバーを実行して正常に表示されるか確認しましょう。
皆様のブラウザーに正常に表示されていることを祈ります!$ npm start // ブラウザーで http://localhost:8080 を開いてくださいおわり
これで開発環境を構築し、Hello worldを表示することができました。
設定ファイルの記述に関しては、見づらくなってしまうため別の記事に書こうかと思います。
最後まで見ていただき、ありがとうございました。
- 投稿日:2020-05-28T10:16:09+09:00
React ContextでuseReducerを使ってみる
はじめに
今回はContext APIを用いた既存のプロジェクトにHooksの一つであるuseReducerを用いてreducerを追加していきます。ドキュメントを読んでもいまいち理解できない方向けの記事にしましたので細かい説明は省いています。そのままコピペしてみても良いかもしれません。
useReducerとは何か
(ContextAPIを用いて作られたグローバルなstateに対して)reducerを設定できるReact Hooksの一つです。つまりContextでreducerを使えます。ただ、そもそもreducerとは何か。
reducerとは何か
もともとはReduxにおいて出てくる考え方で、グローバルなstateの状態を管理する場所です。ローカルなstateの更新はstateを設定したコンポーネント内で出来ますが(下の図参照)、reduxで作成するグローバルなstateはコンポーネント側が直接変更することは出来ません。各コンポーネントはreducerに対して、『この処理を行いたい』という申請を送ると、reducerが代わりにstateの値を更新します。
App.jsimport React, { useState } from 'react'; const App () => { const = [data, setData] = useState('before'); const handleClick = () => { setData('after'); }; return ( <div className="app"> <div>{ data }</div> <button onClick={handleClick}>Change state</button> </div> ) }; export default App;useReducerの使い所
ContextAPIを用いて作られたグローバルなstateの状態を管理する場所としてreducerを設定します。reduxと違いContextはreducerを必要といないため、Context内でstateを更新する関数を定義しても良いのですが、大きなアプリケーションを作る際はstateとそれを更新する関数をreducerを使って分けた方が見やすいコードになり、何よりContextのコード量を減らせます。それでは実際にContextでreducerを設定しましょう。
Context × Reducer
useReducerを追加前のプロジェクト
まずはグローバルなstateであるContextを持つ簡単なReactプロジェクトを作成します。ファイル構成、コードは以下の通りです。以下のプロジェクトにuseReducerを用いてContextにreducerを追加します。
とりあえずはHooksのuseStateを用いてローカルなstateを作成し、Providerにstateを渡しグローバルなstateを作成します。
contexts/UserContext.jsimport React, { createContext, useState } from 'react'; const initialUsers = [ { name: 'Taro', age: 20, id: 1 }, { name: 'Kai', age: 18, id: 2 }, { name: 'Ryo', age: 23, id: 3 } ] export const UserContext = createContext(); const UserContextProvider = ({children}) => { const [users, setUsers] = useState(initialUsers); // もちろん下のようにstateの状態を管理する関数もreducerなしで作れます。 const deleteUser = (id) => { const newUsers = users.filter(user => user.id !== id) setUser([ ...newUsers ]) } return ( <UserContext.Provider value={{users, deleteUser}}> {children} </UserContext.Provider> ) } export default UserContextProvider;子コンポーネント(UserList)をProviderで囲んであげます。
src/App.jsimport React from 'react'; import UserContextProvider from './context/UserContext'; import UserList from './components/UserList'; const App = () => { return ( <UserContextProvider> <UserList /> </UserContextProvider> ); } export default App;stateの中身を簡単に表示させます。
components/UserList.jsimport React, { useContext, Fragment } from 'react'; import { UserContext } from '../context/UserContext'; const UserList = () => { const { users, deleteUser } = useContext(UserContext); const userList = users.map(user => { return ( <section key={user.id}> <h3>{ user.name }</h3> <div>年齢:{ user.age }歳</div> <button type="button" onClick={() => deleteUser(user.id)} >ユーザーを消去</button> </section> ) }) return ( <Fragment> <h2>ユーザーリスト</h2> {userList} </Fragment> ); } export default UserList結果として、ブラウザ上ではこのように表示されます。(黒の枠線はありません。)
useReducerを追加後のプロジェクト
それではuseReducerを用いてContextにreducerを追加します。useReducerを使う前に、reducerを作ります。まずはsrcの下にreducersフォルダを作成し、その中に実際のreducerとなるUserReducer.jsを作ります。dispatch関数を用いて送られてきたオブジェクトをもとにstateを操作します。返り値には更新後のstateを返しています。これだけでは掴みにくいと思うので下のコードで変更した部分を確認してみてください。
src/reducers/UserReducer.js// 下のuserReducerの引数actionは // dispatch関数を用いて送られてきたオブジェクトが入ります export const userReducer = (state, action) => { switch (action.type) { case 'DELETE_USER': // 今回はありませんがusersを含む全てのstateを返し // その後usersの中身を更新しています return { ...state, users: [ ...action.payload ] } default: return state } }contexts/UserContext.jsimport React, { createContext, useReducer } from 'react'; import { userReducer } from '../reducers/UserReducer'; // 変更部分。分かりやすくするためにオブジェクトとします。 const initialState = { users: [ { name: 'Taro', age: 20, id: 1 }, { name: 'Kai', age: 18, id: 2 }, { name: 'Ryo', age: 23, id: 3 } ] } export const UserContext = createContext(); const UserContextProvider = ({children}) => { // 変更部分。useReducerに使いたいreducer、stateの初期値を入れます。 // useStateと似ていてstateとdispatch関数を返します。 const [state, dispatch] = useReducer(userReducer, initialState); // 変更部分。stateとdispatch関数をグローバルで使えるようにします。 return ( <UserContext.Provider value={{state, dispatch}}> {children} </UserContext.Provider> ) } export default UserContextProvider;components/UserList.jsimport React, { useContext, Fragment } from 'react'; import { UserContext } from '../context/UserContext'; const UserList = () => { // 変更部分。 const { state: { users }, dispatch } = useContext(UserContext); // 変更部分。引数のidを用いて、そのidのユーザーを除いたnewUsersを作成。 // dispatch関数を呼び出し、typeを指定し、ペイロードとしてnewUsersを入れる。 const deleteUser = (id) => { const newUsers = users.filter(user => user.id !== id); dispatch({ type: 'DELETE_USER', payload: newUsers }) } const userList = users.map(user => { return ( <section key={user.id}> <h3>{ user.name }</h3> <div>年齢:{ user.age }歳</div> <button type="button" onClick={() => deleteUser(user.id)} >ユーザーを消去</button> </section> ) }) return ( <Fragment> <h2>ユーザーリスト</h2> {userList} </Fragment> ); } export default UserListご覧の通りuseReducer追加前のものと同じ機能を持つアプリができました。これで完成です!
最後に
ここまで読んでいただきありがとうございます。誤字、説明等の間違いがあった場合コメントください。
- 投稿日:2020-05-28T01:48:29+09:00
新人に負けない本棚管理サイト その4(データベース検索編①)
目次
新人に負けない本棚管理サイト その1(プロローグ)
新人に負けない本棚管理サイト その2(環境構築編)
新人に負けない本棚管理サイト その3(トップページ作成編)
新人に負けない本棚管理サイト その4(データベース検索編①)目標
- データベースに問い合わせしてデータを取得する
- 取得したデータを画面に表示する ←今回はここまで
- 検索できるようにする
データの準備
使用するデータベースはMariaDBです。
セットアップ方法はその2を参照してください。テーブル作成
CREATE_TABLE_Books.sqlCREATE TABLE Books( title VARCHAR(100) primary key ,author VARCHAR(20) ,publisher VARCHAR(20) ,finished__at DATE DEFAULT CURDATE() )仮データ登録
CREATE_TABLE_Books.sqlINSERT INTO Books VALUES ('Python Django超入門', '掌田津耶乃', '秀和システム', '2019/08/31'), ('React.js&Next.js超入門', '掌田津耶乃', '秀和システム', NULL), ('PostgreSQL徹底入門 第4版', '近藤雄太 他', '翔泳社', NULL), ('PHPフレームワークLaravel入門 第2版', '掌田津耶乃', '秀和システム', '2019/10/31'), ('PostgreSQLから始めるデータベース生活', 'まぐろ', 'まぐろのみぞおち', '2019/11/03')一緒にSearchResultComponentで仮に入れていた本データを削除します。
import React from 'react'; import { Container, Row, Col, Table } from 'react-bootstrap'; export default class SearchResult extends React.Component { render() { return ( <Container> <Row> <Table striped bordered size="sm"> <thead> <tr> <th>タイトル</th> <th>著者名</th> <th>出版社</th> <th>読了日</th> </tr> </thead> <tbody> {/* 削除 <tr> <td>Python Django超入門</td> <td>掌田津耶乃</td> <td>秀和システム</td> <td>2019/08/31</td> </tr> ... <tr> <td>PostgreSQLから始めるデータベース生活</td> <td>まぐろ</td> <td>まぐろのみぞおち</td> <td>2019/11/03</td> </tr> */} </tbody> </Table> </Row> </Container> ); } }スタートアッププログラムを変更
Expressのインストール
リクエストの処理を定義するために、Node.jsのフレームワークのExpressをインストールします。
terminalnpm install express
スタートアッププログラム作成
まずプロジェクト直下に
server.js
を作ります。
引用:server.jsconst express = require('express') const next = require('next') const dev = process.env.NODE_ENV !== 'production' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare() .then(() => { const server = express(); server.get('*', (req, res) => { return handle(req, res); }) server.listen(3000, (err) => { if(err) { throw err } console.log('> Ready on http://localhost:3000') }) }) .catch((ex) => { console.error(ex.stack) process.exit(1) })次にpackage.jsonのscript部を書き換えます。
"scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js" }そして再度
npm run dev
すると、ターミナルに> Ready on http://localhost:3000
が表示されると思います。
この段階で(開発用の)Webサーバーが立ち上がったということになります。404を解決する
今のままでは、/bookmanagerにアクセスしたあとリロードすると404が表示されます。
これはserver.jsで/bookmanager
にアクセスしたときの処理内容を定義していないため、どのページを表示させるか分からなかったためです。じゃあ定義しよう!!
server.jsに以下のように追記します。
server.js// (略) app.prepare() .then(() => { const server = express(); // 追記ここから server.get('/bookmanager', (req, res) => { return app.render(req, res, '/bookmanager', req.query); }); // 追記ここまで server.get('*', (req, res) => { return handle(req, res); }) server.listen(3000, (err) => { if(err) { throw err } console.log('> Ready on http://localhost:3000') }) }) .catch((ex) => { console.error(ex.stack) process.exit(1) })これにより、
/bookmanager
にgetリクエストが来たらpages/bookmanager/index.js
を表示させるという定義ができました。
何度リロードしても404になりません!!データベースへ問い合わせ
最初にページを開いたときに、データベースから本情報を全件取ってきて表示させます。
メインの内容です!!MariaDBへ接続するためのモジュールを追加
terminalnpm install mariadb
データベース接続情報を定義する
プロジェクト直下に
mariadbConnection.js
を作ります。mariadbConnection.jsconst mariadb = require('mariadb/callback'); const dbConfig = { host: '192.168.xxx.xxx', user: 'username', password: 'yourpassword', database: 'database-name' }; const connection = mariadb.createConnection(dbConfig); module.exports = connection;データベースからデータ取得
bookmanagerページを開いたときにコンソールにデータを表示させてみましょう。
server.jsを書き換えます。server.js// (略) app.prepare() .then(() => { const server = express(); // 変更ここから const connection = require('./mariadbConnection'); server.get('/bookmanager', (req, res) => { let query = "SELECT title, author, publisher, DATE_FORMAT(finished_at, '%Y/%m/%d') AS finished_at FROM Books ORDER BY title"; connection.query(query, (err, rows) => { if(err) throw err; console.log(rows); }); return app.render(req, res, '/bookmanager', req.query); }); // 変更ここまで server.get('*', (req, res) => { return handle(req, res); }) server.listen(3000, (err) => { if(err) { throw err } console.log('> Ready on http://localhost:3000') }) }) .catch((ex) => { console.error(ex.stack) process.exit(1) })
npm run dev
してWebサーバーを再起動します。
その後bookmanagerページを開き、ターミナルを見てみると...いいっすねぇ~(・∀・)
取得結果を画面にバインド(やることが多いです)
通常のWebアプリでは、以下のように検索する仕組みを作ると思います。
- 検索ボタンを押したとき、入力値をサーバーへ送る
- サーバーサイドのプログラムからデータベースへ、検索クエリを投げる
- 結果を画面へ返す
- 画面でイイカンジに表示する
つまり検索するたびにサーバーへリクエストが飛ぶため、ヒット件数が多かったりストアドプロシージャ/ファンクションを介したりする場合はそれなりに時間がかかります。
せっかくNextというエクストリームニュージェネレーションフレームワーク(言いたいだけ)を使っているのに、こんなクラシックなやり方で応答時間を増やすのはダメです!そこで、
「最初にbookmanagerページを開いたときに一度だけ全件取得して、検索するときはそのデータから絞り込む」
ということをしてみます。
これならデータベースへの問い合わせは初めてページを開くときの一度のみになるので、圧倒的にレスポンスが短くなります。(ページをリロードしたときも初めて開いたとする)ページオープン時に全件取得
NextにはgetInitialPropsという便利な関数があります。
カンタンに言うと「ページが表示されたとき一度だけ実行される非同期関数」って感じです。
やりたいことにもってこいの関数ですね。
この中でデータベースへ問い合わせをします。まずbookmanagerページにgetInitialProps関数を追加します。
pages/bookmanager/index.jsimport Layout from '../../components/layout'; import React from 'react'; import { Container, Row, Col, Table, Form } from 'react-bootstrap'; export default class Index extends React.Component { // 追記ここから static async getInitialProps() { const res = await fetch('http://192.168.130.69:3000/bookmanager/get'); const allBooks = await res.json(); return { allBooks }; } // 追記ここまで render() { {/* 略 */} }getInitialPropsの中でAPIを呼び結果を取得しています。
この呼び出しの定義をserver.jsで記述します。server.jsapp.prepare().then(() => { const connection = require('./mariadbConnection'); // データベース呼び出し処理は削除してください。 server.get('/bookmanager', (req, res) => { /* let query = "SELECT title, author, publisher, DATE_FORMAT(finished_at, '%Y/%m/%d') AS finished_at FROM Books ORDER BY title"; connection.query(query, (err, rows) => { if(err) throw err; console.log(rows); }); */ return app.render(req, res, '/bookmanager', req.query); }); // 追記ここから server.get('/bookmanager/get', async (req, res) => { let query = "SELECT title, author, publisher, DATE_FORMAT(finished_at, '%Y/%m/%d') AS finished_at FROM Books ORDER BY title"; connection.query(query, (err, rows) => { if(err) throw err; res.json(rows); // レスポンスに取得データを持たせる }); }); // 追記ここまで server.get('*', (req, res) => { return handle(req, res); }); server.listen(3000, (err) => { if(err) { throw err; } console.log('> Ready on http://localhost:3000'); }); }) .catch((ex) => { console.error(ex.stack); process.exit(1); });毎度のことながら、
npm run dev
してアクセスします。
データベースには以下のデータが入っています。
title author publisher finisher_at PHPフレームワークLaravel入門 第2版 掌田津耶乃 秀和システム 2019/10/31 PostgreSQLから始めるデータベース生活 まぐろ まぐろのみぞおち 2019/11/03 PostgreSQL徹底入門 第4版 近藤雄太 他 翔泳社 NULL Python Django超入門 掌田津耶乃 秀和システム 2019/08/31 React.js&Next.js超入門 掌田津耶乃 秀和システム NULL プリンをもっと見守る技術 M5StickVで体験するAIの世界 aNo研 aNo研 2020/05/26 アクセスすると...
ちゃんと表示されました(;^ω^)
お疲れ様でした
盛りだくさんで書くのも大変でした。
日本語を書くのがへたっぴなので、分かりづらいところがあると思います。
小さい目標を1つずつ達成して、
「現段階で何ができているのか」「次に何ができてほしいか」「ゴールまで到達するにはあと何ができなければならないか」
をまとめるようにすると、ワカラナイパニックに陥ることは少ないと思います。と、私の会社の先輩が言っていました。
次回
条件を入力して検索できるようにします。