4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

日本一わかりやすいReact-Redux講座 実践編 #1~2 学習備忘録

Posted at

はじめに

この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

ここから、いよいよ本格的にアプリを開発していきます。

前回記事はこちら

要約

  1. 店舗:ユーザー = 1:n のECアプリを開発する
  2. Firebase Authによる新規登録、サインイン認証を実装する
  3. 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に移動。

image.png

メール/パスワードの鉛筆マークから、本機能を有効化します。

image.png
image.png
image.png

これで有効化ができました!とっても簡単。

Firebase用の設定ファイルを作る

ここからは、アプリ側でコードを書いていきます。

まず最初に、有効化したFirebase Auth機能をアプリと連携する設定変数を取得します。

コンソールの歯車マーク->プロジェクトを設定
image.png

Firebase SDK snippet構成を選択すると、設定変数が取得できます。

image.png

アプリ側でfirebase用のファイルを作成します。srcディレクトリ直下にfirebaseディレクトリを作り、ここに設定用ファイルを定義します。

src
 └─ firebase
     ├─ config.js
     └─ index.js  
src/firebase/config.js
export const firebaseConfig = {
  apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  databaseURL: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  projectId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  storageBucket: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  messagingSenderId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  appId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  measurementId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
};

先ほどの設定変数を保存するファイル。文頭でexportしている点は注意。

src/firebase/index.js
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を読み込む記述が残っているため、削除。

src/index.js
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ディレクトリ
assets
 ├── img
 │   ├── icons
 │   │   └── logo.png
 │   └── src
 │       ├── no-profile.png
 │       └── no_image.png
 ├── reset.css
 ├── style.css
 └── theme.js

cssファイルとしては、今後のこのreset.cssstyle.cssを使用していきます。

これらを読み込む記述をsrc/App.jsxに加えます。

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を活用して作成します。

src/components/UIkit/PrimaryButton.jsx
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
src/components/UIkit/TextField.jsx
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
src/components/UIkit/index.js
export {default as PrimaryButton} from "./PrimaryButton"
export {default as TextInput} from "./TextInput"

各UIコンポーネントは、親コンポーネントからもらう props に応じて、諸々のパラメーター(ラベルや行数など)を変えられるようにしてあります。

上記のUIコンポーネントを利用して、SignUpテンプレートを作成します。

src/templates/SignUp.jsx
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)を定義します。

src/Router.jsx
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画面で一通りできたはずです。ブラウザで確認してみると、
image.png

いい感じにできています!アカウント登録ボタンを押してみると、
image.png

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. コンポーネントファイルに関数を設置しましょう。

src/templates/SignUp.jsx
.
.
.
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.jssignUp()を定義します。

src/reducks/users/operations.js
.
.
.
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側のとの通信を簡単に実装できる
  • uidunique idの略。auth.createUserWithEmailAndPassword()を実行した時点で自動的に生成される(resultの中に含まれる)
  • usersコレクションのうち、上記のuidのところへ、各引数を保存する。保存が完了したら、ルートへリダイレクトする

これでsignUp()関数の設定は完了しました。しかし、もう一つ追加でやることがあります。

現時点では、Cloud Firestore へのデータの書き込みが禁止された設定になっているため、それをいったん解除する必要があります。

データの書き込み・読み込み権限はfirestore.rulesで定義します。

./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側にデプロイする必要があります。

terminal
$ firebase deploy --only firestore:rules

ここまでやれば準備完了です!ブラウザから実際にアカウント登録を実行してみます。
image.png

アカウント登録ボタンを押すと、ルートへリダイレクトされます。firebaseコンソールから、実際にアカウントが作成されたかを確認します。

Authentication

image.png

Database

image.png

アカウント登録が完了していますね!

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を新規作成したため追記が必要。
src/templates/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
  • サインインに必要な情報はemailpasswordのみなので、usernameconfirmPasswordの入力フォーム(及びそれらを管理するためのstate)を削除
  • サインインボタンを押すことで、operations.jsで定義するsignIn()関数が発火するように記述
src/reducks/users/operations.js
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の更新を行う。
src/reducks/users/actions.js
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カラムを追加している。
src/reducks/users/reducers.js(変更箇所なし)
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:が対応しています。

src/reducks/store/store.js(変更箇所なし)
.
.
.
export default function createStore(history) {
  return reduxCreateStore(
    combineReducers({
      router: connectRouter(history),
      users: UsersReducer
    }),
.
.
.

参考として掲載。UsersReducerからStoreに指令が来ることで、サインイン時のユーザー情報をもとにstateが更新されます。

src/reducks/store/initialState.js
const initialState = {
  users: {
    isSignedIn: false,
    role:  "",
    uid: "",
    username: ""
  }
};

export default initialState

  • roleカラムを追加している。
src/templates/index.js
export {default as Home} from './Home'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'
  • Loginを削除し、SignInを追加。
src/Router.jsx
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/

image.png

initialStateで定義した通り、ユーザーID、ユーザー名がブランクの状態です。

サインイン画面(/signin)より、先ほどアカウント登録したユーザー情報を用いてサインインを行います。

localhost:3000/signin

image.png

サインインボタンを押すと、ルートへリダイレクトされます。

image.png

先ほど登録したアカウントのユーザーID,ユーザー名が表示されていればOKです!

おわり

再度要点をまとめると、

  1. 店舗:ユーザー = 1:n のECアプリを開発する
  2. firebase.authによる新規登録、サインイン認証を実装する
  3. reducksパターンに沿った関数の実装手順を理解する

以上です!次回は認証のリッスンによる state の永続化を行います。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?