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

## はじめに
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『[日本一わかりやすいReact-Redux講座 実践編](https://www.youtube.com/watch?v=8fNgXnVTULU)』の学習備忘録です。

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

前回記事は[こちら](https://qiita.com/ddpmntcpbr/items/9e04aa602858bbaa3180)。

## 要点
- 商品登録ページを作成(CRUDのC)
- 商品登録用関数``saveProduct()``を定義
- Cloud Firestoreへのデータ保存では、``doc/set/add``メソッドを使用する。

## #4 Firestoreにデータをadd/setしよう

おさらいですが、今回の開発アプリのデータ設計は以下の通りでした。

```:データ設計
1. categoriesコレクション
2. productsコレクション
3. usersコレクション
  ├── 3-1. cartサブコレクション
  └── 3-2. ordersサブコレクション
```

これらのうち、``2. productsコレクション``については、以下の通りのCRUD機能を実装します。

- Create: 商品情報の追加
- Read: 商品情報の読み込み
- Update: 商品情報の更新
- Delete: 商品情報の削除

本講座では、``Create: 商品情報の追加``に対応するページを作っていきます。

### 商品登録ページのテンプレートを作成

まず、Viewから作成します。

先に実装画面を見せると、こんな感じになります。

**``商品登録ページ``**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/725dc909-b064-8e50-1671-16fe686560d5.png)

店舗商品として登録する商品の情報をフォームに入力し、「商品情報を保存」をクリックすることで、DBに商品情報をが登録(CRUDのC)される流れです。

入力フォームの内、「商品名」「商品説明」「価格」に関しては、すでに定義済みのUIコンポーネントである``TextInput.jsx``で実装できそうです。

対して、「カテゴリー」「性別」については、いくつかの選択肢から入力値を決める**セレクトボックス形式**の入力フォームとして実装すべきです。

こちらに関しては``TextInput.jsx``では実装できなさそうなので、新たに``SelectBox.jsx``というコンポーネントを定義することにします。

```:新規作成・編集ファイル
1. src/components/UIkit/SelectBox.jsx
2. src/components/UIkit/index.js
```

```react:src/components/UIkit/SelectBox.jsx
import React from "react";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import {makeStyles} from "@material-ui/styles"

const useStyles = makeStyles({
  formControl:{
    marginBottom: 16,
    minWidth: 128,
    width: "100%"
  }
})

const SelectBox = (props) => {
  const classes = useStyles();

  return (
    <FormControl className={classes.formControl}>
      <InputLabel>{props.label}</InputLabel>
      <Select
        required={props.required} value={props.value}
        onChange={(event) => props.select(event.target.value)}
      >
        {props.options.map((option) => (
          <MenuItem key={option.id} value={option.id}>{option.name}</MenuItem>
        ))}
      </Select>
    </FormControl>
  )
}

export default SelectBox
```

- Material UI の ``FormControl``,``InputLabel``,``MenuItem``, ``Select``を組み合わせて実装。
- ``props.options``として親コンポーンネントから渡されたものをmapメソッドで展開することで、リストを実現している。

**``※動作イメージ``**
![SelectBox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/13fbf21d-2cb8-e7d7-3a97-ea4110152672.gif)

```javascript:src/components/UIkit/index.js
export {default as PrimaryButton} from "./PrimaryButton"
export {default as SelectBox} from "./SelectBox" //追記
export {default as TextInput} from "./TextInput"
```

これで``SelectBox``コンポーネントが完成しました。

これを踏まえて、商品登録ページのViewを作成します。


```:新規作成・編集ファイル
1. src/templates/ProductEdit.jsx　//テンプレートファイル
2. src/components/src/templates/index.js //エントリーポイント
3. src/components/src/Router.jsx //ProductEditテンプレートのルーティングを定義
```

```react:src/templates/ProductEdit.jsx
import React,{useState,useCallback} from "react";
import {PrimaryButton,SelectBox,TextInput} from "../components/UIkit";

const ProductEdit = () => {

  const [name, setName] = useState(""),
        [description, setDescription] = useState(""),
        [category, setCategory] = useState(""),
        [gender, setGender] = useState(""),
        [price, setPrice] = useState("");

  const inputName = useCallback((event) => {
    setName(event.target.value)
  },[setName])

  const inputDescription = useCallback((event) => {
    setDescription(event.target.value)
  },[setDescription])

  const inputPrice = useCallback((event) => {
    setPrice(event.target.value)
  },[setPrice])

  const categories = [
    {id:"tops", name:"トップス"},
    {id:"shirt", name:"シャツ"},
    {id:"pants", name:"パンツ"}

  ]
  const genders = [
    {id:"all", name:"すべて"},
    {id:"male", name:"メンズ"},
    {id:"female", name:"レディース"}
  ]

  return (
    <section>
      <h2 className="u-text__headline u-text-center">商品の登録・編集</h2>
      <div className="c-section-container">
        <TextInput
          fullWidth={true} label={"商品名"} multiline={false} required={true}
          onChange={inputName} rows={1} value={name} type={"text"}
          />
        <TextInput
          fullWidth={true} label={"商品説明"} multiline={true} required={true}
          onChange={inputDescription} rows={5} value={description} type={"text"}
          />
        <SelectBox
          label={"カテゴリー"} required={true} options={categories} select={setCategory}
        />
        <SelectBox
          label={"性別"} required={true} options={genders} select={setGender}
        />
        <TextInput
          fullWidth={true} label={"価格"} multiline={false} required={true}
          onChange={inputPrice} rows={1} value={price} type={"number"}
          />
      </div>
      <div className="module-spacer--medium" />
      <div className="center">
        <PrimaryButton
          label={"商品情報を保存"}
          onClick={() => {console.log('Clicked!')}}
        />
      </div>
    </section>
  )
}

export default ProductEdit
```

- name, description, price に関してはこれまで通り、``<TextInput>``を利用して実装
- category, gender については、``<SelectBox>``で実装。選択肢は定数として定義。
- 「商品情報を登録」ボタンの onClick イベントには、ダミーとして``console.log('Clicked!')``を設置

```javascript:src/components/src/templates/index.js
export {default as Home} from './Home'
export {default as ProductEdit} from './ProductEdit' //追記
export {default as Reset} from './Reset'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'

```

```react:src/components/src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,ProductEdit,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={Home} />
        <Route exact path={"/product/edit"} component={ProductEdit} />　//追記
      </Auth>
    </Switch>
  );
};

export default Router
```

- ``(/product/edit)``をルーティング。
- 認証ユーザー（特に店舗側のユーザー）しか利用できないようにしたいので、``<Auth>``でラッピングする。

以上でViewの実装は完了です。動作確認します。

**``http://localhost:3000/product/edit``**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/fa38f3be-0653-a93d-0649-7e9003b4716c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/d522dfe1-630e-237c-c060-aca8f5e411b6.png)

いい感じにできています！また、「商品情報を保存」をクリックすると、デベロッパーツールにコンソールに``Clicked!``が表示されます。

### saveProduct関数の実装

#### products state の追加
「商品情報を保存」ボタンの中身を作っていきます。

入力したフォームを受け取り、DBに保存する関数を定義すればよさそうです。

reducksパターンに則って考えると、この関数は``productsカテゴリー``に関するものになるため、**src/reducks/products/operations.js**に記述するべき、と分かります。

しかし、現時点のreducksパターンにおいて、``products``という state を管理するファイルは未定義です。

というわけで、 ``products`` という新しい state を管理するためのReduxファイルを、reducksパターンに則って作りましょう。

必要な初期設定は以下の通りです。

```
1. src/reducks 直下に products ディレクトリを作成
2. src/reducks/products 直下に actions.js, operations.js, reducers.js, selectors.js, (type.js) を作成
3. src/reducks/store/initialState.js で、products の初期値を定義
4. src/reducks/products/reducers.js で ProductsReducers を定義
5. src/reducks/store/store.js で ProductsReducers を読み込む
```

```javascript:src/reducks/store/initialState.js
const initialState = {
  products: {
    list: []
  },
.
.
.
};

export default initialState
```

```javascript:src/reducks/products/reducers.js
import initialState from '../store/initialState'

export const ProductsReducer = (state = initialState.products, action) => {
  switch (action.type) {
    default:
      return state
  }
}
```

```javascript:src/reducks/store/store.js
.
.
.
import {ProductsReducer} from "../products/reducers"; //追記
.
.
.

export default function createStore(history) {
  return reduxCreateStore(
    combineReducers({
      router: connectRouter(history),
      users: UsersReducer,
      products:ProductsReducer //追記
    }),
.
.
.
  )
}
```

これで、 products state が Redux の Store と接続されました。

actions.js, operations.js, selectors.jsは、ひとまず空白ファイルとして保存しておきます。

### saveProduct()関数の実装

reducksパターンによる関数実装をおさらいすると、

```:編集ファイル
1. コンポーネントファイル
2. operations.js
3. actions.js
4. reducers.js
```

でした。

ただし、今回は「入力テキストを state として受け取って画面描画」などはしませんので、``3. actions.js``, ``4. reducers.js``はいったんノータッチです。

```react:src/templates/ProductEdit.jsx
.
.
.
      <div className="center">
        <PrimaryButton
          label={"商品情報を保存"}
          // onClick={() => {console.log('Clicked!')}}
          onClick={() => dispatch(saveProduct(name, description, category, gender, price))}
        />
      </div>
.
.
.
```

PrimaryButtonのonClickイベントに、``saveProduct``関数を設定します。

``saveProduct``はoperationsで定義するものなので、dispatchを噛ませる必要があります。

```javascript:src/reducks/products/operations.js
import {db, FirebaseTimestamp} from "../../firebase";
import { push } from "connected-react-router";

const productsRef = db.collection("products")

export const saveProduct = (name,description,category,gender,price) => {
  return async (dispatch) => {
    const timestamp = FirebaseTimestamp.now()

    const data = {
      category: category,
      description: description,
      gender: gender,
      name: name,
      price: parseInt(price, 10),
      updated_at: timestamp
    }

    const ref = productsRef.doc();
    const id = ref.id;
    data.id = id
    data.created_at = timestamp

    return productsRef.doc(id).set(data)
      .then(() => {
        dispatch(push('/'))
      }).catch((error) => {
        throw new Error(error)
      })
  }
}
````

``saveProduct``関数を定義します。

- 外部DBと通信を行う処理のため、``return async (dispatch) => {...``。
- 引数は文字列として渡ってくるため、``parseInt(price, 10),``で十進数のint型数値へ変換。
- **``set()``メソッド**を使うことで、Cloud firestore にデータを保存。

### ※ add/set/docメソッドの役割
DB(Cloud firestore) にデータを保存するとき、

(1) DB内で、データを保存するための場所(=ID)を自動で採番する
(2) そのIDの場所にデータを保存する

という流れで処理が行われる。

このとき、

- (1)を行う: **``doc()``**メソッド
- (2)を行う: **``set()``**メソッド
- (1)(2)を一気に行う: **``add()``**メソッド

という役割のメソッドが存在する。**``doc()``**+**``set()``**=**``add()``**と考えると分かりやすい。そのため、

```javascript:doc&setメソッド
const data = {...}
const ref = db.collection("products").doc();
const id = ref.id;

return db.collection("products").doc(id).set(data)
```

これと、

```javascript:addメソッド
const data = {...}

return db.collection("products").add(data)
```

これは最終的な結果は同じになる。

単純にデータを保存したいだけであれば**``add()``**の方で問題ないが、今回のアプリでは「自動採番されたIDをフィールド値として保存したい」ため、**``doc()``**+**``set()``で処理を行っている。

(**``add()``**を使用すると、IDの自動採番と同時にデータの保存処理が行われるので、IDをデータ内部に取り込む処理が行えない)

先の``operations.js``のうち、

```javascript:src/reducks/products/operations.js
.
.
.
    const ref = productsRef.doc();
    const id = ref.id;
    data.id = id
.
.
.
```

の箇所で、自動採番されたIDをdataの中に追加している。

### 動作確認
saveProduct()の動作確認をしてみます。

**``http://localhost:3000/product/edit``**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/ec86d03b-7f97-a5c9-c557-a5613bbbca1f.png)

入力フォームにテキストを入れて、「商品情報を保存」をクリックします。

**``http://localhost:3000/``**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/c3e43d0e-5186-8da1-63ee-ea1d1c7b5809.png)

saveProduct()が正常に動作すれば、ルートへリダイレクトします。

実際にデータが保存されたのか、Firebaseコンソールから確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540216/0ff8b8a1-8059-59e3-1643-49309cbbbc8d.png)

先ほどの入力テキストが保存されています！

入力値に加え、Cloud Firestore上で自動採番されたID(zUTHkCmsEm1ov0ahrUTo)が、idカラムとして保存されていることも確認できます！

### まとめ
本講座の要点をまとめると、
- 商品登録ページを作成(CRUDのC)
- 商品登録用関数``saveProduct()``を定義
- Cloud Firestoreへのデータ保存では、``doc/set/add``メソッドを使用する。