LoginSignup
3
2

More than 3 years have passed since last update.

【備忘録】日本一わかりやすいReact-Redux講座 実践編 #7 「商品を一覧表示しよう」

Last updated at Posted at 2020-08-11

はじめに

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

前回まで講座で、Products に対する CRUD のうち、C(:Create), Update(:Update)を実装しました。今回の講座では、CRUD の 一覧表示 R(:Read)、及び D(:Delete)を実装します。

※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #6 「useEffectで編集機能を作ろう」

要点

  • Reactでは、親コンポーネントで子コンポーネントをイテレートする形でリスト一覧表示を実装する。
  • Material-UI における <Card/> で、画像+文字列のコンポーネントを実装する。
  • Material-UIにおける <Menu/> で、モーダル開閉を含めたメニューバーを実装する。

完成系イメージ

http://localhost:3000/

image.png

いままで殺風景だったルートページ("/")に、Products を一覧表示します。各Productの登録画像のうち一つがサムネイルとなって表示されます。

image.png

メニューを開くと「編集する」「削除する」ボタンが表示されます、。「編集する」をクリックすると、各Productsの編集ページ("/products/edit/:id")へ遷移します。

「削除する」をクリックすると、

image.png

Productが削除され、残りのProductsで一覧表示がされます。

#7「商品を一覧表示しよう」

reducksファイル

Productsの一覧リスト表示を実装するにあたり、まずreducksファイルを定義します。

operations.js内でDBから Products を一括で取得する関数(fetchProducts())を定義し、取得結果を Store に保存するよう、actions.jsreducers.jsに追記していきます。

また、Store内のProductsをビュー側で参照できるセレクターをselectors.jsに定義していきます。

実装ファイル(reducks)
1.src/reducks/products/operations.js
2.src/reducks/products/actions.js
3.src/reducks/products/reducers.js
4.src/reducks/products/selectors.js
1.src/reducks/products/operations.js
import { fetchProductsAction } from "./actions";
.
.
.
const productsRef = db.collection("products")
.
.
.
export const fetchProducts = () => {
  return async (dispatch) => {
    productsRef.orderBy("updated_at","desc").get()
      .then(snapshots => {
        const productList = []
        snapshots.forEach(snapshot => {
          const product = snapshot.data();
          productList.push(product)
        })
        dispatch(fetchProductsAction(productList));
      })
  }
}

  • orderBy("updated_at","desc") で、DBから取得した結果を更新日時に対する昇順に整理し、fetchProductsActionへ渡します。
2.src/reducks/products/actions.js
export const FETCH_PRODUCTS = "FETCH_PRODUCTS";
export const fetchProductsAction = (products) => {
  return {
    type: "FETCH_PRODUCTS",
    payload: products
  }
}
3.src/reducks/products/reducers.js
import * as Actions from './actions'
import initialState from '../store/initialState'

export const ProductsReducer = (state = initialState.products, action) => {
  switch (action.type) {
    case Actions.FETCH_PRODUCTS:
      return {
        ...state,
        list: [...action.payload]
      };
    default:
      return state
  }
}
  • list: ...action.payloadではなく、list: [...action.payload]とのように、新しい配列として定義することで、配列の格納先メモリが切り替わり、 state が変更されたことを Component 側で検知できるようにしています。
4.src/reducks/products/selectors.js
import {createSelector} from "reselect";

const productsSelector = (state) => state.products;

export const getProducts = createSelector(
  [productsSelector],
  state => state.list
)
  • getProducts()で、Store内の Products を Component 側から参照できるようにします。

ビューファイル

続いてビューファイルを作成します。

Reactにおいて、「DBの中身をリストで一覧表示」を実装するときは、親コンポーネントで子コンポーネントをイテレートするという実装が一般的です。

今回は、親:ProductList.jsx、子:ProductCard.jsxとして、リスト表示を実装します。

image.png

実装ファイル(ビュー関連)
1.src/Router.jsx
2.src/templates/ProductList.jsx
3.src/templates/index.js
4.src/components/Products/ProductCard.jsx
5.src/components/Products/index.js
1.src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {ProductEdit,ProductList,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={ProductList} /> {/* 修正 */}
        <Route path={"/product/edit(/:id)?"} component={ProductEdit} />
      </Auth>
    </Switch>
  );
};

export default Router

ルートページ("/")をProductListテンプレートに設定します。

2.src/templates/ProductList.jsx
import React,{useEffect} from "react";
import {ProductCard} from "../components/Products";
import {useDispatch, useSelector} from "react-redux";
import {fetchProducts} from "../reducks/products/operations"
import {getProducts} from "../reducks/products/selectors"

const ProductList = () => {
  const dispatch = useDispatch();
  const selector = useSelector((state) => state);
  const products = getProducts(selector);

  useEffect (() => {
    dispatch(fetchProducts())
  },[]);

  return (
    <section className="c-section-wrapin">
      <div className="p-grid__row">
        {products.length > 0 &&(
          products.map(product => (
            <ProductCard
              key={product.id} id={product.id} name={product.name}
              images={product.images} price={product.price}
            />
          ))
        )}
      </div>
    </section>
  )
}

export default ProductList
  • useEffect()により、初期レンダー時において、先ほどoperations.jsで定義したfetchProducts()を実行します。その後、const products = getProducts(selector);でDBから取得した商品情報がproductsに格納されます。
  • products.length > 0 &&...で、productsが1つ以上存在する場合のみ、以下の要素をレンダーするよう条件分岐させます。

  • products.map(product => (...))で、mapメソッドを用いて配列の中身をイテレートし、子コンポーネントの<ProductCard />に渡します。イテレートする際は、子コンポーネントには一意の値をkeyとして渡す必要があるため、ユニークな値であるproduct.idkeyに設定します。

3.src/templates/index.js
export {default as Home} from './Home'
export {default as ProductEdit} from './ProductEdit'
export {default as ProductList} from './ProductList' //追記
export {default as Reset} from './Reset'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'
  • ProductListをエントリーポイントに追加。
4.src/components/Products/ProductCard.jsx
import React, {useState} from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography';
import NoImage from "../../assets/img/src/no_image.png";
import {push} from "connected-react-router";
import {useDispatch} from "react-redux";
import IconButton from "@material-ui/core/IconButton";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import MoreVertIcon from '@material-ui/icons/MoreVert';
import {deleteProduct} from "../../reducks/products/operations";

const useStyles = makeStyles((theme) => ({
  root: {
    [theme.breakpoints.down("sm")]: {
      margin: 8,
      width: "calc(50% - 16px)"
    },
    [theme.breakpoints.up("sm")]: {
      margin: 16,
      width: "calc(33.333% - 32px)"
    }
  },
  content: {
    display: "flex",
    padding: "16 8",
    textAlign: "left",
    "&:last-child": {
      paddingBottom: 16
    }
  },
  media: {
    height: 0,
    paddingTop: "100%"
  },
  price: {
    color: theme.palette.secondary.main,
    fontSize: 16
  }
}));

const ProductCard = (props) => {
  const classes = useStyles();
  const dispatch = useDispatch();

  const [anchorEl, setAnchorEl] = useState(null);

  const handleClick = (event) => {
    setAnchorEl(event.currentTarget)
  };

  const handleClose = () => {
    setAnchorEl(null)
  };

  const images = (props.images.length > 0) ? props.images : [{path: NoImage}];
  const price = props.price.toLocaleString();

  return (
    <Card className={classes.root}>
      <CardMedia
        className={classes.media}
        image={images[0].path}
        title=""
        onClick={() => dispatch(push("/product/" + props.id))}
      />
      <CardContent className={classes.content}>
        <div onClick={() => dispatch(push("/product/" + props.id))}>
          <Typography color="textSecondary" component="p">
            {props.name}
          </Typography>
          <Typography className={classes.price} component="p">
            ¥{price}
          </Typography>
        </div>
        <IconButton onClick={handleClick}>
          <MoreVertIcon/>
        </IconButton>
        <Menu
          anchorEl={anchorEl}
          keepMounted
          open={Boolean(anchorEl)}
          onClose={handleClose}
        >
          <MenuItem
            onClick={() => {
              dispatch(push("/product/edit/" + props.id))
              handleClose()
            }}
          >
            編集する
          </MenuItem>
          <MenuItem
            onClick={() => {
              dispatch(deleteProduct(props.id))
              handleClose()
            }}
          >
            削除する
          </MenuItem>
        </Menu>
      </CardContent>
    </Card>
  )
}

export default ProductCard

Materila-UIコンポーネントを多く使用しているため、補足します。

image.png

<Card>コンポーネントで、各リストアイテムを表示しています。画像(<CardMedia />)、文字列(<CardContent />)の配置、及びサイズは任意に調整可能です。

<IconButton />をクリックすることでhandleClick()が発火し、setAnchorEl()anchirElに値がセットされることで、モーダルが開きます。

image.png

「編集する」をクリックするとdispatch(deleteProduct(props.id))が発火し、該当するProductsの編集ページへ遷移します。

「削除する」をクリックすると、dispatch(deleteProduct(props.id))が発火し、該当するProductsの削除処理が行われます(deleteProduct()はこのあと実装)

5.src/components/Products/index.js
export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ProductCard} from "./ProductCard" //追記
export {default as SetSizeArea} from "./SetSizeArea"
  • ProductCardコンポーネントをエントリーポイントに追加。

ここまでで、一覧表示は一通り完成しました!

最後に、Productsに対するCRUDのDとして、deleteProduct()を実装します。

商品を削除しよう

削除の実装は簡単です。reduckcパターンに則り、以下のファイルに削除処理を定義していきます。

実装ファイル(削除処理)
1.src/reducks/products/operations.js
2.src/reducks/products/actions.js
3.src/reducks/products/reducers.js
1.src/reducks/products/operations.js
import { fetchProductsAction,deleteProductAction } from "./actions";
.
.
.
const productsRef = db.collection("products")

export const deleteProduct = (id) => {
  return async(dispatch, getState) => {
    productsRef.doc(id).delete()
      .then(() => {
        const prevProducts = getState().products.list;
        const nextProducts = prevProducts.filter(product => product.id !== id)
        dispatch(deleteProductAction(nextProducts))
      })
  }
}
.
.
.
  • productsRef.doc(id).delete()とすることで、該当するidの商品情報を、Cloud Firestore上から削除します。
  • getState()により、現時点でのprosuctsを取得します。その後、filterメソッドで該当する商品情報を配列から削除し、deleteProductAction ()に渡します。
2.src/reducks/products/actions.js
export const DELETE_PRODUCT = "DELETE_PRODUCT";
export const deleteProductAction = (products) => {
  return {
    type: "DELETE_PRODUCT",
    payload: products
  }
}
.
.
.
3.src/reducks/products/reducers.js
import * as Actions from './actions'
import initialState from '../store/initialState'

export const ProductsReducer = (state = initialState.products, action) => {
  switch (action.type) {
    // 追記
    case Actions.DELETE_PRODUCT:
      return {
          ...state,
          list: [...action.payload]
      };
    // 追記ここまで
    case Actions.FETCH_PRODUCTS:
      return {
        ...state,
        list: [...action.payload]
      };
    default:
      return state
  }
}

これでProductCard.jsx内で記述したdeleteProducts()関数が正常に動作するようになり、削除機能が実装されます。

動作確認

動作自体は完成系イメージと同様のため割愛します。

deleteProducts()により、Cloud Firestore上でも商品情報の削除が行われているだけ、確認します。、

http://localhost:3000/
image.png

Firebaseコンソール -> Database
image.png

productsが4つ保存されています。メニューから「削除する」をクリックします。

image.png

image.png

商品情報が表示されなくなりました。Firebaseコンソールを確認すると、

image.png

確かに商品情報が削除されています!

さいごに

今回の要点を整理すると、

  • Reactでは、親コンポーネントで子コンポーネントをイテレートする形でリスト一覧表示を実装する。
  • Material-UI における <Card/> で、画像+文字列のコンポーネントを実装する。
  • Material-UIにおける <Menu/> で、モーダル開閉を含めたメニューバーを実装する。

今回は以上です!

このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。

3
2
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
3
2