はじめに
今までフロントエンドの技術を中心に勉強しており、ReactやNext.jsの学習を進めてきました。
その中で、状態管理はContext + Hooksを用いていましたが、この度、「Reduxも学習しよう」と思い、ECサイト作成を通して学習した記録です。
作成したもの
React × Redux × Firebaseで、ECサイトを作成しました。
一店舗対お客様の、洋服のオンラインショップです。
ロゴは、某衣料品ブランドのロゴを模して作成しました。
[URL]
https://react-redux-ec.vercel.app/
(ゲストログイン用アカウント)
メールアドレス:guest@example.com
パスワード:guestguest
[github]
https://github.com/suzu1997/react-redux-ec
トラハックさんの以下の動画シリーズを基に、学習・ECサイト作成を行いました。
トラハックさん、上質な講座をありがとうございます。
使用技術
主な使用技術は以下の通りです。
- TypeScript 4.1.2
- React 17.0.2
- react-router 5
- connected-react-router 6.9.2
- Redux 4.1.2
- Firebase 9.6.0
- Cloud Firestore
- Firebase Authentication
- Storage
- Tailwind CSS
- Material UI(MUI)
機能
サインイン/サインアップ
Firebase Authenticationによってメール/パスワード認証を行なっています。
sendPasswordResetEmailメソッドを用いて、パスワードを忘れた時でも新しいパスワードにすぐ変更できるようにしています。
商品一覧ページ
販売中の商品が並んだページ。
メンズ/レディース、トップス・アウターなどのカテゴリごとの絞り込み機能、並び替え機能(更新順、価格の高い順、低い順)、キーワード検索機能をつけました。
商品詳細ページ
商品の詳細を載せたページ。
商品画像の表示にはreact-id-swiperというライブラリを使用し、複数の画像をスライダーで閲覧できるようになっています。
サイズを選択して、ショッピングカートへの追加やお気に入りへの追加を行うことができます。
ショッピングカート/購入機能
商品詳細でショッピングカートへ追加した商品をリストで確認、リストからの削除、購入の機能があります。
購入の処理時には、Firestoreのトランザクションを使用しており、処理が失敗するとロールバック(全ての購入処理をリセット)されるようになっています。これにより商品の在庫や購入履歴等でのデータの乖離を阻止し、よきせぬトラブルに備えています。
管理者機能
管理者でログインした時に、商品の登録、編集、削除が行えます。
商品の商品画像追加時には、FirebaseのStorageに画像がアップロードされます。
(管理者用アカウント)
メールアドレス:admin@gmail.com
パスワード:aaaaaaaa
プロフィールページ
登録されている会員情報が見れるページです。
ここで住所や電話番号、メールアドレスなどを変更することもできます。
レスポンシブ対応
レスポンシブにも対応し、スマホでもデスクトップでも快適に閲覧できるようにしました。
デスクトップ版では左側にメニュー、右側にバナーを配置した3カラム構成に。
スマホ版ではドロワーメニューを実装しました。
その他機能
- 通知バナーでの通知
Reactのライブラリであるreact-hot-toastを使用して、ログイン/ログアウト時、カートやお気に入り追加時等に、通知バナーを表示するようにしました。
画面の端から現れて、数秒すると消える通知です。
これにより、ユーザーの操作を邪魔することなく、通知を行うことができます。
- ログインフィルター
ログインしなくても閲覧できるページ、ログイン後しか閲覧できないページを作成しました。
商品一覧や詳細はログインしなくても見ることができます。
ログインしていない状態でお気に入りやカート追加をしようとすると、モーダルで
ログインを促すような仕様にしました。
Reduxについて軽くまとめ
今回のECサイト作成はReduxを学習することが目的だったので、Reduxについてメモがてら軽くまとめようと思います。
Reduxとは
Reduxは、Reactが扱うstate(状態)を管理する状態管理ライブラリです。
データフロー設計の一つである、Fluxの思想を適用しています。
Fluxとはデータの流れを一方向に限定した考え方で、それによりデータの流れがわかりやすくなり、管理がしやすくなります。それにより、アプリ全体の見通しが良くなるというメリットがあります。
Reduxにおいてもデータは一方向のみに流れ、変更や取得が行われます。
ディレクトリ構成
Reduxのディレクトリ構成は、大きく3パターンあるようです。
- redux-way : reduxによって導入される概念ごとに分ける
- ducks : redux-wayのAction関連を1つにまとめる stateカテゴリーごとに一つのファイル
- re-ducks : stateのカテゴリーごとにディレクトリを作成し、その中にAction関連ファイルを分割
私はトラハックさんの動画でも紹介されていた、re-ducksパターンで作成しました。
redux-way, ducksの後で、改善されて出てきたパターンがre-ducksパターンであり、中規模・大規模開発に向いているのもre-ducksパターンみたいです。
src/
├ components
| ├ ....
| └ ....
├ containers
| ├ ....
| └ ....
└ reducks
├ store
| ├ initialState.ts
| └ store.ts
|
├ products
| ├ actions.ts
| ├ operations.ts
| ├ reducers.ts
| ├ selectors.ts
| └ types.ts
└ users
├ actions.ts
├ operations.ts
├ reducers.ts
├ selectors.ts
└ types.ts
各ファイル/機能について
actions.ts
Actionsとは、アプリ側からの要求を受けて、Storeへ状態の変更の依頼をする役割を担う機能です。
コンポーネントでイベントが発生するタイミングで、action.tsに定義したActions関数が呼ばれ、次のReducersにデータが渡されます。
export const FETCH_PRODUCTS_IN_CART = 'FETCH_PRODUCTS_IN_CART';
export const fetchProductsInCartAction = (products: Array<ProductInCart>) => {
// プレーンなオブジェクトを返す
return {
// typeとpayloadを記述する
type: 'FETCH_PRODUCTS_IN_CART',
payload: products
}
};
どんな種類の変更なのかを表すtypeと、storeの変更に必要なデータであるpayloadをオブジェクト形式で返します。
reducers.ts
Reducersとは、Actionsからデータを受け取り、Storeのstateをどう変更するのか決める役割を担う機能です。
実行されたActionを受け取り、Switch構文内でaction.typeに応じてStoreにstateを渡します。
import * as Actions from './actions';
import initialState from '../store/initialState';
import { Action } from './types';
// 第一引数にState(現在のstateの状態)。初期状態をデフォルトとして設定
// 第二引数にactionがreturnした値(typeとpayload)
export const UsersReducer = (state = initialState.users, action: Action) => {
// Actionsのtypeに応じてstateをどう変更するのか決める
switch (action.type) {
case Actions.DELETE_FAVORITE_PRODUCTS:
return {
...state,
favorite: [...action.payload],
}
case Actions.FETCH_FAVORITE_PRODUCTS:
return {
...state,
favorite: [...action.payload],
}
case Actions.FETCH_ORDERS_HISTORY:
・
・
・
operations.ts
ここでは、Storeのデータを変更するための複雑な処理を記述します。
redux-thunkというライブラリを使用して、非同期処理を制御します。データベースへのアクセス時などです。
コンポーネントでイベントが発生するとまず、operationsファイルのメソッドが呼ばれ、その中でActions関数がよばれることになります。
selectors.ts
ここでは、Storeで管理しているstateを参照する関数を定義します。reselectというnpmモジュールを使って実装します。
import { createSelector } from 'reselect';
const usersSelector = (state) => state.users;
// userIdを参照する関数
export const getUserId = createSelector(
[ usersSelector ],
state => state.uid // state: usersSelectorが返す値(state.users)
);
selectorで取得される値は、store内のstateが変更される度にコンポーネント側でも再取得されるため、常に最新のstateがコンポーネント側で取得されます。
types.ts
TypeScriptで記述する場合に使うファイルで、型定義を記述します。
action, operation, reducerなどで使う型を定義しておきます。
storeディレクトリ
storeディレクトリのinitialState.tsでは、storeのデータの初期値を定義しておきます。
また、store.tsでは、ReduxのstoreとReducersを関連づける処理を行います。
使用したライブラリ
connected-react-router
connected-react-routerとは、Reduxのstoreでrouterを管理するためのライブラリです。
SPAでURLルーティングを行うためのReactのライブラリ、react-routerをベースにルーティング状態を管理します。
connected-react-routerを使用することで、pushやreplaceといったメソッドを使って画面遷移を行なったり、現在のpath情報を簡単に取得できたりします。
redux-thunk
上述の通り、Reduxで非同期処理を行うことのできるミドルウェアです。
所感
Reduxを使用すると、たくさんのメソッドを介して状態が変更・管理されることになり、ファイル数が非常に多くなるなーという印象でした。
アプリが大規模になってデータ数やそれを用いるところが多くなると、有用な手段なのかなと思いました。
ただ、個人レベルで何か作成するとなると、多くの記事で記載されているように、Context + hooksで実装するのが簡単でいいかなと感じます。
今後はuseSWRやreact queryでの状態管理も学習していきたい所存です。
大変だったこと・苦労したこと
-
TypeScriptでの型定義 :
Reduxを用いていると、特殊な型定義が必要な場面が多々出てきました。
間違った型を指定しまうとエラーが出るため、適切な型を調べて付与するのが大変でした。 -
Firebaseのバージョンが動画の時期から上がりv9になっており、書き方が大幅に変わっていたため、ドキュメントと睨めっこしながら記述するのが大変でした。