概要
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact入門【実践編】』の自分用学習備忘録です。
本動画シリーズが、react初心者である私にとって非常に分かりやすいものでしたので、少しでも世の中に広めたいという想いを込めて公開します!
学習初期での筆者習熟レベル
- Reactについては、同チャンネルの『日本一わかりやすいReact入門』のみ学習済み
- Firebaseは以前少し触ったことがある程度
#1...Reactでチャットボットを作ろう
開発アプリ概要
- チャットボットアプリを開発
- 予めセンテンス用のデータセットは用意しておき、それらを決められたルールで返信をする
使用技術
-
React
- Class
- Functional
- create-react-app
- Hooks
-
Firebase
- Hosting
- Firebase
- Clound Functions
- Material-UI
- Slack(Incoming Webhook)
Material-UI, Slack Webhookについては完全初見です。
#2...サクッと環境構築しよう
create-react-appで環境を構築。今回のアプリ名は『chatbot-demo』。
$ npx create-react-app chatbot-demo
アプリのディレクトリに移動して、npmサーバーを起動。
$ npm start
localhost:3000へブラウザでアクセスし、いつもの画面が出てくれば、無事react環境で出来上がっています!
続いて、Material-UIをnpmへインストール。
$ npm install --save @material-ui/core @material-ui/icons @material-ui/system
今回は、Material-UIのRoboto Font
とFont Icons
を使用したいので、headタグ内に追記。
<head>
...
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
</head>
これで環境構築は完了!
#3...Firebaseプロジェクトの作成と初めてのデプロイ
Firebaseとは?
- Googleが提供するmBaaS
- DB管理からアプリのデプロイまで、バックエンド側の諸々をお任せできる、兎に角なんかすごいサービス
- Firebaseを利用することで、エンジニア自身はフロントエンド側のコーディングに集中できる
Firebase -ブラウザ上の設定-
Settings -> リロケーションリソースをasia-northeast1
(要するに日本のこと)に設定。
Database -> Cloud Firestoreでデータベースを作成(データベースは本番環境用の方に設定したが、セキュリティールールは別途設定するので、テスト環境用にしても恐らく関係ない)
Firebase -ターミナル上での設定-
ローカル全体にfirebase-toolsをインストール。
$ npm install -g firebase-tools
今回作成したアプリディレクトリ内に、firebaseをインストール。これによりpackage.json
内に"firebase"
が追加される。
$ npm install --save firebase
firebaseにログインする。
$ firebase login
create-react-appで作成したローカル上のアプリと、ブラウザから作成したfirebase上のアプリを接続する。
$ firebase init
- Firebase CLI featuresからは、Firestore、Functions、Hostingを選択。
- Cloud Functionsの言語はTypescriptを選択。
-
? What do you want to use as your public directory?
はデフォルトではpublic
になっているが、build
に変更する。なぜなら、create-react-appにおいては、publicは雛形用のディレクトリであり、本番用ディレクトリはbuildと決められているから。 -
? Configure as a single-page app (rewrite all urls to /index.html)?
はYesを選択。今回はSPAとして開発するため。 - 上記以外はデフォルトでOK
Firebaseセキュリティルールの設定
現時点では、誰でもDBへの書き込みができる危険な状態になっているので、認証済みユーザーのみが書き込みできるよう設定。
...
allow read;
allow write: if request.auth.uid != null;
...
Firebaseへデプロイ
アプリの本番環境用ディレクトリを作成。
$ npm run build
コンパイルエラーを避けるため、下記をコメントアウト
// import * as functions from 'firebase-functions';
...
Firebaseへデプロイする。
$ firebase deploy
デプロイが完了したら、Hosting URL:
のところに表示されているURLを開き、確認してみる。
create-react-appの初期画面で出ていればOK!! Firebaseは本当にデプロイが簡単ですねー
#4...stateの設計とクラスコンポーネントの作成
stateの設計
- ルートで管理するstate(React側)とデータモデル(Firebase側)が1対1で対応するよう、設計する。
- データモデルの設計はViewから始めるのが効率的。所謂「ワイヤーフレーム」から作り始める。
動画内ではstate設計の答えが最初から示されていました。ただ、なぜその答えに辿り着いたかを自分なりに納得したかったので、今回は自力でも考えてみました。
1頁目 | 2頁目 |
---|---|
結果、state設計をまとめると以下の通り。
answers: [{key: value}, {key, value}...]
answer: {
content: string, //回答内容
nextId: string, //次のcurentId
}
chats: [{key: value}, {key, value}...]
chat: {
text: string, //チャット本文
type: string //質問か回答か
}
currentId: string
dataset: {
"currentId_1": {
answers: [
{content:"content_x", nextId:"currentId_x"}
.
.
.
{content:"content_y", nextId:"currentId_y"}
],
question: "question content_x",
}
"currentId_2": {
...
}
open: boolean(true or false)
最後のopenはお問い合わせフォームのモーダル開閉に使用。
stateの定義
stateの設計をApp.jsxに定義する。create-react-appでは、App.jsはFunctional Componentで定義されているが、stateを扱いたいため、Class Componeneに書き換える(拡張子もjsからjsxに変更する)
import React from 'react';
import './App.css';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
answers: [],
chats: [],
currentId: "init",
dataset: {},
open: false
}
}
render(){
return (
<div>
</div>
);
}
}
datasetはgithubからコピーして、src/dataset.jsとして保存。
const defaultDataset = {
"init": {
answers: [
{content: "仕事を依頼したい", nextId: "job_offer"},
{content: "エンジニアのキャリアについて相談したい", nextId: "consultant"},
{content: "学習コミュニティについて知りたい", nextId: "community"},
{content: "お付き合いしたい", nextId: "dating"},
],
question: "こんにちは!🐯トラハックへのご用件はなんでしょうか?",
},
"job_offer": {
answers: [
{content: "Webサイトを制作してほしい", nextId: "website"},
{content: "Webアプリを開発してほしい", nextId: "webapp"},
{content: "自動化ツールを作ってほしい", nextId: "automation_tool"},
{content: "その他", nextId: "other_jobs"}
],
question: "どのようなお仕事でしょうか?",
},
"website": {
answers: [
{content: "問い合わせる", nextId: "contact"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "Webサイト細作についてですね。コチラからお問い合わせできます。",
},
"webapp": {
answers: [
{content: "問い合わせる", nextId: "contact"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "Webアプリ開発についてですね。コチラからお問い合わせできます。",
},
"automation_tool": {
answers: [
{content: "問い合わせる", nextId: "contact"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "自動化ツール開発についてですね。コチラからお問い合わせできます。",
},
"other_jobs": {
answers: [
{content: "問い合わせる", nextId: "contact"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "その他についてですね。コチラからお問い合わせできます。",
},
"consultant": {
answers: [
{content: "YouTubeで動画を見る", nextId: "https://www.youtube.com/channel/UC-bOAxx-YOsviSmqh8COR0w"},
{content: "学習コミュニティについて知りたい", nextId: "community"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "トラハックは普段からYouTubeでキャリアについて発信しています。また、僕が運営するエンジニア向け学習コミュニティ内でも相談に乗っていますよ。",
},
"community": {
answers: [
{content: "どんな活動をしているの?", nextId: "community_activity"},
{content: "コミュニティに参加したい", nextId: "https://torahack.web.app/community/"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "2020年3月から学習コミュニティを始めました!🎉Webエンジニアへの転職を目指す人向けに、プログラミングを教えたりキャリアの相談に乗っています。",
},
"community_activity": {
answers: [
{content: "さらに詳細を知りたい", nextId: "https://youtu.be/tIzE7hUDbBM"},
{content: "コミュニティに参加したい", nextId: "https://torahack.web.app/community/"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "フロントエンド向けの教材の提供、キャリアや勉強法に関するメルマガの配信、週1のオンライン作業会などを開催しています!\n詳細はYouTube動画で紹介しています。",
},
"dating": {
answers: [
{content: "DMする", nextId: "https://twitter.com/torahack_"},
{content: "最初の質問に戻る", nextId: "init"}
],
question: "まずは一緒にランチでもいかがですか?DMしてください😘",
},
}
export default defaultDataset
defaultDataset
という定数を定義し、export default
でファイル外部から利用できるようしている。これをApp.jsにimportする。
import React from 'react';
import './App.css';
import defaultDataset from "./dataset"
...
css周りを設定
-
githubより
assets/styles/style.css
をコピー - App.cssを削除。App.jsxのcss importを上記ファイルに修正
- index.cssをassets/stylesへ移動。index.jsのimport文を修正
css設定を終えたら、早速App.jsx内でjsxタグを書いてみる
import React from 'react';
import './assets/styles/style.css';
import defaultDataset from "./dataset"
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
answers: [],
chats: [],
currentId: "init",
dataset: defaultDataset,
open: false
}
}
render() {
return (
<section className="c-section">
<div className="c-box">
</div>
</section>
);
}
}
localhost:3000へ行くと、空の四角い箱が画面に表示されているはずです。
#5...繰り返し再利用できる関数コンポーネントを作ろう
画面下部に表示させる回答部分をコンポーネントで作る。コンポーネントの構造としては、
- AnswersList
- Answer
- Answer
- Answer
- ...
のようなイメージ。選択肢ひとつひとつがAnswerコンポーネントであり、それらを束ねる親コンポーネントとしてAnswersListコンポーネントを定義する。
作成・修正するファイルは、
- src/App.jsx
- components/AnswersList.jsx
- components/Answer.jsx
- components/index.js
components/index.jsはいわゆるエントリーポイントの役割を担う。全てのコンポーネントを読み込む場所であり、コンポーネントをimportしたい場合は(コンポーネントの種類に依らず)参照先をここにしておけばいい
完成形はこちら。
import React from 'react';
import './assets/styles/style.css';
import defaultDataset from "./dataset"
import {AnswersList} from "./components/index"
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
answers: [],
chats: [],
currentId: "init",
dataset: defaultDataset,
open: false
}
}
initAnswer = () => {
const initDataset = this.state.dataset[this.state.currentId];
const initAnswers = initDataset.answers;
this.setState({
answers: initAnswers
})
}
componentDidMount() {
this.initAnswer()
}
render() {
return (
<section className="c-section">
<div className="c-box">
<AnswersList answers={this.state.answers} />
</div>
</section>
);
}
}
- AnswersListコンポーネントをimportしている。今回はコンポーネントファイルを直接読みに行かず、エントリーポイントである
components/index.js
を経由している - AnswersListにdatasetの値が渡されるまでの時系列を整理すると、
- App.jsxのMountingが開始。まず最初にconstructor()が走り、各stateに初期値が与えられる
- constructor()の次はrender()が走り、JSX構造が作られる。この時点では、
this.state.answers
の中身はconstructor()で定義されたanswers: []
となっている - render()の次はcomponentDidMount()が走る。initAnswer()関数が実行されることで、answersの中にdatasetの値が代入される
- ライフサイクルのうち、「最初に1回だけ実行したい」という処理は、componentDidMount()に書く
import 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} key={index.toString()} />
})}
</div>
)
}
export default AnswersList
- App.jsxより受けとってprops.answersをmap関数を用いて繰り返し処理
import 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">
{props.content}
</Button>
)
}
export default Answer
- Material-UIの公式HPから、buttonをコピーして貼り付け。
export {default as AnswersList} from './AnswersList'
export {default as Answer} from './Answer'
- 全てのコンポーネントを読み込ませる。
ここまでで、dataset内のテキストが含まれたボタンが表示されるようになる。
記事が長くなったのでいったんここまで!
続きは『日本一わかりやすいReact入門【実践編】#6~9 学習備忘録』です。