はじめに
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。
ここから、いよいよ本格的にアプリを開発していきます。
前回記事はこちら。
要約
-
店舗:ユーザー = 1:n
のECアプリを開発する -
Firebase Auth
による新規登録、サインイン認証を実装する -
reducksパターン
に沿った関数の実装手順を理解する
#1 ECアプリの機能とデータ設計
実装するアプリ概要
- ファッション系のECアプリ。服や靴などが出品される。
-
店舗:ユーザー = 1:n
- 店舗は商品の出品、在庫管理、売り上げ管理ができる
- ユーザーは商品閲覧、カートへ追加、購入、注文履歴の確認ができる
ユニクロや無印のECアプリのように、商品の出品者が固定の設計(メルカリやヤフオクのように、ユーザー自身が出品者になれるものが、n:n のECアプリ)
実装する機能の整理
現時点では完璧には中身を理解できていませんが、後で振り返られるよう、動画内容に沿ってまとめておきます。
認証機能
ユーザーの新規作成、ログイン、ログアウト機能を実装する。Firebase Auth
を利用します。
認証のリッスン
認証情報をブラウザに残すことで、再ログインの手間を省く。アプリ全体をAuth
コンポーネントでラッピングして実装します。
商品情報のCRUD
- Create: 商品情報の追加
- Read: 商品情報の読み込み
- Update: 商品情報の更新
- Delete: 商品情報の削除
Firebase の Cloud Function
を DB として利用する。商品情報の操作は、管理者ユーザーしか行えないようにします。
react-swiper による画像スライダー
一つの商品情報に対して複数の画像を対応させたい(例えば、同じ型のTシャツの色違い、など)ので、動的な画像スライダーを作ります。
Drawer メニュー
ヘッダーコンポーネントとして、表示・非表示を動的に制御できるメニューを作る。Material-UI
を活用します。
カートへの商品追加
商品をカートに追加することで、カートアイコン上に表示される"アイテム数"を変更させる。Firestore
でデータをリッスンします。
商品の注文
トランザクションを実装します( Firestore
を利用)。加えて、注文履歴の閲覧機能も作ります。
タグ検索機能
「メンズ」「レディース」「トップス」「ボトムス」のようなタグを用いた検索機能を作ります。Firestore indexes
を活用する。
データ設計
1. categoriesコレクション
2. productsコレクション
3. usersコレクション
├── 3-1. cartサブコレクション
└── 3-2. ordersサブコレクション
categoriesコレクション
商品情報をグループ分けするためのタグの情報を保存する。
productsコレクション
商品情報を保存する。
usersコレクション
ユーザー情報の保存する。
cartsサブコレクション
対応するユーザーのカートも中身の情報を保存する。
orders サブコレクション
対応するユーザーの注文履歴の情報を保存する。
#2 Firebase Authで認証機能を作ろう
Firebase Authとは
Firebaseの中に含まれている、認証機能を簡単に実装するための機能。非常に少ないコード量で処理が複雑な認証機能を実装できます。
パスワード認証、電話番号認証、各種OAuth認証(gmail, twitter, facebookなど)などの複数の認証方法に対応しています。
今回は最も基本的なメール/パスワード認証
を実装します。
ConsoleからAuthcenticationを設定する
まず最初に、Firebase側でFirebase Authの機能を有効します。
Firebaseコンソールから、Authenticationに移動。
メール/パスワード
の鉛筆マークから、本機能を有効化します。
これで有効化ができました!とっても簡単。
Firebase用の設定ファイルを作る
ここからは、アプリ側でコードを書いていきます。
まず最初に、有効化したFirebase Auth機能をアプリと連携する設定変数を取得します。
Firebase SDK snippet
で構成
を選択すると、設定変数が取得できます。
アプリ側でfirebase用のファイルを作成します。srcディレクトリ直下にfirebase
ディレクトリを作り、ここに設定用ファイルを定義します。
src
└─ firebase
├─ config.js
└─ index.js
export const firebaseConfig = {
apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authDomain: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
databaseURL: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
projectId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
storageBucket: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
messagingSenderId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
appId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
measurementId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
};
先ほどの設定変数を保存するファイル。文頭でexport
している点は注意。
import firebase from "firebase/app";
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/storage';
import 'firebase/functions';
import {firebaseConfig} from "./config";
firebase.initializeApp(firebaseConfig);
export const auth = firebase.auth();
export const db = firebase.firestore();
export const storage = firebase.storage();
export const functions = firebase.functions();
export const FirebaseTimestamp = firebase.firestore.Timestamp;
- firebaseの各種機能と、先ほど定義した
firebaseConfig
をimportする -
firebase.initializeApp(firebaseConfig);
と書くことで、reactアプリとfirebaseインスタンスを接続する(接続先のfirebaseインスタンスを定義する) - firebaseの各種機能を定数化し、外部ファイルで使用できるようexportする
ファイルを整理
ここまでの講座で作成したファイルやcreate-react-app
で自動作成されたファイルのうち、不要なものを削除しておきます。
## css関連ファイル。cssは別に用意されたものを使うため
src/App.css
src/index.css
## テスト関連ファイル。今回はテストは扱わないため。
src/App.test.js
src/logo.svg
src/setup.js
## 基礎編#10で作成したコンテナーコンポーネント。今後はHooksのみを使用する。
src/containers/index.js
src/containers/Login.js
src/templates/LoginClass.jsx
src/index.js
で、src/App.css
を読み込む記述が残っているため、削除。
import * as History from 'history';
// import './index.css'; // 削除
import App from './App';
import * as serviceWorker from './serviceWorker';
本講座のデモアプリが公開されているgithubリポジトリ(https://github.com/deatiger/ec-app-demo)から、css関連ファイルを含むsrc/assets
ディレクトリをダウンロードし、開発アプリのsrc
ディレクトリ直下に配置する。
assets
├── img
│ ├── icons
│ │ └── logo.png
│ └── src
│ ├── no-profile.png
│ └── no_image.png
├── reset.css
├── style.css
└── theme.js
cssファイルとしては、今後のこのreset.css
やstyle.css
を使用していきます。
これらを読み込む記述をsrc/App.jsx
に加えます。
import React from 'react'
import Router from './Router'
import "./assets/reset.css"
import "./assets/style.css"
.
.
.
SignUp画面を作る
template として SignUpコンポーネントを作成するにあたり、必要になる UI コンポーネントを先に用意します。
componentsディレクトリの下にUIkit
ディレクトリを作成し、その中で複数のコンポーネントで使用するための UI コンポーネントをまとめて管理するようにします。
components
└── UIkit
├── PrimaryButton.jsx // ボタンのUIコンポーネント。SignUp画面では、アカウント登録のボタンとして使う
├── TextInput.jsx // テキスト入力フィールド。
└── index.js // エントリーポイント
PrimaryButtonコンポーネント、TextInputコンポーネントは、Material-UIを活用して作成します。
import React from "react";
import Button from "@material-ui/core/Button";
import {makeStyles} from "@material-ui/styles";
const useStyles = makeStyles({
"button": {
backgroundColor: "#4dd0e1",
color: "#000",
fontSize: 16,
height: 48,
marginButton: 16,
width: 256
}
})
const PrimaryButton = (props) => {
const classes = useStyles();
return(
<Button className={classes.button} variant="contained" onClick={() => props.onClick()}>
{props.label}
</Button>
)
}
export default PrimaryButton
import React from 'react';
import TextField from '@material-ui/core/TextField'
const TextInput = (props) => {
return (
<TextField
fullWidth={props.fullWidth}
label={props.label}
margin={"dense"}
multiline={props.multiline}
required={props.required}
rows={props.rows}
value={props.value}
type={props.type}
onChange={props.onChange}
/>
)
}
export default TextInput
export {default as PrimaryButton} from "./PrimaryButton"
export {default as TextInput} from "./TextInput"
各UIコンポーネントは、親コンポーネントからもらう props に応じて、諸々のパラメーター(ラベルや行数など)を変えられるようにしてあります。
上記のUIコンポーネントを利用して、SignUpテンプレートを作成します。
import React, {useState,useCallback} from "react";
import {PrimaryButton,TextInput} from "../components/UIkit"
const SignUp = () => {
const [username,setUsername] = useState()
const [email,setEmail] = useState()
const [password,setPassword] = useState()
const [confirmPassword,setConfirmPassword] = useState()
const inputUsername = useCallback((event) => {
setUsername(event.target.value)
},[setUsername])
const inputEmail = useCallback((event) => {
setEmail(event.target.value)
},[setEmail])
const inputPassword = useCallback((event) => {
setPassword(event.target.value)
},[setPassword])
const inputConfirmPassword = useCallback((event) => {
setConfirmPassword(event.target.value)
},[setConfirmPassword])
return(
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">アカウント登録</h2>
<div className="module-spacer--medium" />
<TextInput
fullWidth={true} label={"ユーザー名"} multiline={false}
required={true} rows={1} value={username} type={"text"} onChange={inputUsername}
/>
<TextInput
fullWidth={true} label={"メールアドレス"} multiline={false}
required={true} rows={1} value={email} type={"email"} onChange={inputEmail}
/>
<TextInput
fullWidth={true} label={"パスワード"} multiline={false}
required={true} rows={1} value={password} type={"password"} onChange={inputPassword}
/>
<TextInput
fullWidth={true} label={"パスワード(確認用)"} multiline={false}
required={true} rows={1} value={confirmPassword} type={"password"} onChange={inputConfirmPassword}
/>
<div className="module-spacer--medium" />
<div className="center">
<PrimaryButton
label={"アカウントを登録する"}
onClick={() => console.log("Clicked!")}
/>
</div>
</div>
)
}
export default SignUp
- テキスト入力フィールドを扱うときは、各入力値を受ける state を
useState ()
で定義し、useCallback()
で永続化する(実践編#12のおさらい) - 「アカウントを登録する」ボタンの onClick イベントには、本来はアカウント登録処理を行うイベントを埋め込むが、今はダミーで
console.log("Clicked!")
を設置
作成した SignUp に対応するルーティング(/signup
)を定義します。
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,Login,SignUp} from "./templates";
const Router = () => {
return (
<Switch>
<Route exact path={"/signup"} component={SignUp} />
<Route exact path={"/login"} component={Login} />
<Route exact path={"(/)?"} component={Home} />
</Switch>
);
};
export default Router
これで、SignUp画面で一通りできたはずです。ブラウザで確認してみると、
いい感じにできています!アカウント登録ボタンを押してみると、
console.log("Clicked!")
が発火しています!
アカウント登録機能を作る
今回講座の肝です。先ほどのアカウント登録ボタンの中身を作ります。
一般に、reducksパターンにおいては state に関わる処理を定義する場合、以下の4ファイルを新規作成or修正します
1. コンポーネントファイル
2. operations.js
3. actions.js
4. reducers.js
1. コンポーネントファイル
は、ここではまさに先ほど作成したSignUp.jsx
です。
この中に、実装したい処理の発火点を作ります。今回のケースでは、 アカウント登録処理を行う関数である signUp() を、「アカウントを登録する」ボタンの onClickイベントに埋め込む ということになります。
2. operations.js
で、実際に実行したい処理を記述します。今回のケースでは引数のバリデーションを確認した上で、DB(Cloud firestore)にユーザー情報を登録する関数であるsignUp()を定義するということになります。
3. actions.js
, 4. reducers.js
で、2. operations.js
で処理をされた値を元に Store内の state の変更を行います。
しかし今回のケースでは、「signUp()関数は user state に関わる処理のため reducks/users/operations.js に記述をするが、アカウント登録に使用したユーザー情報は画面描画には使用せず、登録後はすぐにルートにリダイレクトさせるため state を更新する必要はない」という状況のため、3. actions.js
, 4. reducers.js
についてはノータッチになります(こういうケースは珍しい方かと思います)
後に作るサインイン機能は、実際に state の更新まで行うので、上記流れで実装をしますので、そのときに改めて解説します。
まず、1. コンポーネントファイル
に関数を設置しましょう。
.
.
.
import {signUp} from "../reducks/users/operations"
import {useDispatch} from "react-redux"
const SignUp = () => {
const dispatch = useDispatch()
.
.
.
return(
<div className="c-section-container">
.
.
.
<div className="center">
<PrimaryButton
label={"アカウントを登録する"}
onClick={() => dispatch(signUp(username,email,password,confirmPassword))}
/>
</div>
</div>
)
}
アカウント登録を行う関数として定義されるsignUp()
に対して、必要な引数を渡してonClickイベントに設置します。
このsignUp()
はこれから作成するもので、これはoperations.js
に記述します。
operations.js
に定義した関数をコンポーネントで利用するためには、Hooksの一種であるuseDispatch()
を使う必要があります。
次に、operations.js
にsignUp()
を定義します。
.
.
.
import {push} from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "../../firebase/index"
.
.
.
export const signUp = (username,email,password,confiramPassword) => {
return async (dispatch) => {
if (username === "" || email === "" || password === "" || confiramPassword === "") {
alert("必須項目が未入力です")
return false
}
if (password !== confiramPassword) {
alert("パスワードが一致しません。もう一度お試しください")
return false
}
return auth.createUserWithEmailAndPassword(email,password)
.then(result => {
const user = result.user
if (user) {
const uid = user.uid
const timestamp = FirebaseTimestamp.now()
const userInitialData = {
created_at: timestamp,
email: email,
role: "customer",
uid: uid,
updated_at: timestamp,
username: username
}
db.collection("users").doc(uid).set(userInitialData)
.then(()=>{
dispatch(push("/"))
})
}
})
}
}
-
src/firebase/index.js
で定数化したauth
,db
,FirebaseTimestamp
をインポート - 引数に対するバリデーションを実施。空欄のものがあるときか、パスワードが不一致のときは
false
を返して処理を終了させる -
async
を入れて非同期処理を制御(DB通信時のお約束) -
auth.createUserWithEmailAndPassword()
で、メール/パスワード認証によるfirebase側のとの通信を簡単に実装できる -
uid
はunique id
の略。auth.createUserWithEmailAndPassword()
を実行した時点で自動的に生成される(resultの中に含まれる) - usersコレクションのうち、上記の
uid
のところへ、各引数を保存する。保存が完了したら、ルートへリダイレクトする
これでsignUp()関数の設定は完了しました。しかし、もう一つ追加でやることがあります。
現時点では、Cloud Firestore へのデータの書き込みが禁止された設定になっているため、それをいったん解除する必要があります。
データの書き込み・読み込み権限はfirestore.rules
で定義します。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if request.auth.uid != null;
allow create;
allow update: if request.auth.uid == userId;
allow delete: if request.auth.uid == userId;
}
}
}
usersコレクションに対して、createはいつでも可能、read、update, deleteはユーザー認証状態でのみ可能、と言う設定になっています。
これを有効化するため、firestore.rules
のみをいったんfirebase側にデプロイする必要があります。
$ firebase deploy --only firestore:rules
ここまでやれば準備完了です!ブラウザから実際にアカウント登録を実行してみます。
アカウント登録ボタンを押すと、ルートへリダイレクトされます。firebaseコンソールから、実際にアカウントが作成されたかを確認します。
Authentication
Database
アカウント登録が完了していますね!
Sign in 画面を作る
Sign Up画面をベースに、Sign in画面も作っていきます。修正するファイルは以下の通り。
繰り返しになりますが、reducksパターンにおいては、state に関わる処理を定義する場合、以下の4ファイルを新規作成or修正します。
1. コンポーネントファイル
2. operations.js
3. actions.js
4. reducers.js
1.コンポーネントファイル
として、SignIn.jsx
を新たに作成します。この中に、2. operations.js
で定義するsignIn()
関数を設置したボタンを配置します。
2. operations.js
で、signIn()
関数を追記します。この関数は、引数として渡されたユーザー情報を元にDB(Cloud firebase)と通信をしてユーザー情報を取得し、それをactionsjsへ渡すという役割を担います。
3.actions.js
および4. reducers.js
で、2. operations.js
でDBから取ってきたユーザー情報を元に Store 内の state を更新する記述を行います。
今回は、すでにサインイン処理についてある程度記述がされています。3.actions.js
についてのみ、一部修正をします。
以上の流れで実装を行います。実際に新規作成or修正するファイルをまとめると、
1. src/templates/SignIn.jsx
2. src/reducks/users/operations.js
3. src/reducks/users/actions.js
4. src/reducks/store/initialState.js // actions.jsの修正に応じて一部修正
5. src/templates/index.js // エントリーポイント。SignIn.jsxを新規作成したため追記が必要。
6. src/Router.jsx // SignIn画面のルーティング。SignIn.jsxを新規作成したため追記が必要。
import React, {useState,useCallback} from "react";
import {PrimaryButton,TextInput} from "../components/UIkit"
import {signIn} from "../reducks/users/operations"
import {useDispatch} from "react-redux"
const SignIn = () => {
const dispatch = useDispatch()
const [email,setEmail] = useState()
const [password,setPassword] = useState()
const inputEmail = useCallback((event) => {
setEmail(event.target.value)
},[setEmail])
const inputPassword = useCallback((event) => {
setPassword(event.target.value)
},[setPassword])
return(
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">サインイン</h2>
<div className="module-spacer--medium" />
<TextInput
fullWidth={true} label={"メールアドレス"} multiline={false}
required={true} rows={1} value={email} type={"email"} onChange={inputEmail}
/>
<TextInput
fullWidth={true} label={"パスワード"} multiline={false}
required={true} rows={1} value={password} type={"password"} onChange={inputPassword}
/>
<div className="module-spacer--medium" />
<div className="center">
<PrimaryButton
label={"サインイン"}
onClick={() => dispatch(signIn(email,password))}
/>
</div>
</div>
)
}
export default SignIn
- サインインに必要な情報は
email
とpassword
のみなので、username
とconfirmPassword
の入力フォーム(及びそれらを管理するためのstate)を削除 - サインインボタンを押すことで、operations.jsで定義する
signIn()
関数が発火するように記述
import { signInAction } from "./actions";
import {push} from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "../../firebase/index"
export const signIn = (email,password) => {
return async (dispatch) => {
if (email === "" || password === "" ) {
alert("必須項目が未入力です")
return false
}
auth.signInWithEmailAndPassword(email,password)
.then(result => {
const user = result.user
if (user) {
const uid = user.uid
db.collection("users").doc(uid).get()
.then(snapshot => {
const data = snapshot.data()
dispatch(signInAction({
isSignedIn: true,
role: data.role,
uid: uid,
username: data.username
}))
dispatch(push("/"))
})
}
})
}
}
.
.
.
-
auth.signInWithEmailAndPassword()
でサインイン認証が行える。 - 上記メソッドの返り値をもとに、DBから具体的なユーザー情報を取り出す。
- ユーザー情報を
signInAction
に渡すことで、stateの更新を行う。
export const SIGN_IN = "SIGN_IN";
export const signInAction = (userState) => {
return {
type: "SIGN_IN",
payload: {
isSignedIn: true,
role: userState.role,
uid: userState.uid,
username: userState.username
}
}
};
.
.
.
-
operations.js
より引数として渡されたユーザー情報を受け取るよう記述。 -
role
カラムを追加している。
import * as Actions from './actions'
import initialState from '../store/initialState'
export const UsersReducer = (state = initialState.users, action) => {
switch (action.type) {
case Actions.SIGN_IN:
return {
...state,
...action.payload
};
default:
return state
}
}
参考として掲載。先のアクションはcase Actions.SIGN_IN:
が対応しています。
.
.
.
export default function createStore(history) {
return reduxCreateStore(
combineReducers({
router: connectRouter(history),
users: UsersReducer
}),
.
.
.
参考として掲載。UsersReducer
からStoreに指令が来ることで、サインイン時のユーザー情報をもとにstateが更新されます。
const initialState = {
users: {
isSignedIn: false,
role: "",
uid: "",
username: ""
}
};
export default initialState
-
role
カラムを追加している。
export {default as Home} from './Home'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'
- Loginを削除し、SignInを追加。
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,SignIn,SignUp} from "./templates";
const Router = () => {
return (
<Switch>
<Route exact path={"/signup"} component={SignUp} />
<Route exact path={"/signin"} component={SignIn} />
<Route exact path={"(/)?"} component={Home} />
</Switch>
);
};
export default Router
- SignInテンプレートに対するルーティング(/signin)を追加。
これにより、サインイン画面が完成したはずです。ブラウザで、まずルート(/)を見てみます。
localhost:3000/
initialStateで定義した通り、ユーザーID、ユーザー名がブランクの状態です。
サインイン画面(/signin)より、先ほどアカウント登録したユーザー情報を用いてサインインを行います。
localhost:3000/signin
サインインボタンを押すと、ルートへリダイレクトされます。
先ほど登録したアカウントのユーザーID,ユーザー名が表示されていればOKです!
おわり
再度要点をまとめると、
-
店舗:ユーザー = 1:n
のECアプリを開発する -
firebase.auth
による新規登録、サインイン認証を実装する -
reducksパターン
に沿った関数の実装手順を理解する
以上です!次回は認証のリッスンによる state の永続化を行います。