5
4

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/Firebaseを使った積み上げカウンターアプリ

Last updated at Posted at 2020-03-29

#はじめに

2019年12月末にそれまで興味を持っていたプログラミングの学習を開始し、そのうちに楽しくなってきたので2月頃からエンジニア転職に向けて本格的な学習を開始した。
教材としてオンライン教材のFrontHacksを使用し、React/Reduxを学習。
一通りの学習が完了したので、可能な限り学習した内容を盛り込んだアプリを作成した。
アプリの一連の内容の振り返りの意味も込めて、簡単にアプリの内容と実装した機能の紹介をさせていただきます。

https://daily-timemanegement.firebaseapp.com
(E-mail:1234@gmail.com , Password:12345678)

Github
https://github.com/Ken-Takahashi-go/Invest-Expense-Counter

まだ初学者ですので、言葉足らずな部分や理解が間違っている部分などあるかと思いますがご容赦ください。
何かありましたらご連絡ください。

#1、技術要素
・React
・React-router
・React-redux
・redux-thunk
・Firebase (Hosting,Authentication,Database)
・Material-UI

教材では、React,React-Redux,Redux-thunkを学習したが、データベースとの連携とログイン機能をつけてみたいと思ったのでFirebaseを使用してみることにした。

#2、アプリ概要
その日その日にやったことをそれぞれカテゴライズしてカウントしていくというシンプルなアプリ
(ログイン機能・データベース連携あり)
<カテゴリ>
 ①自己投資した時間
 ②浪費した時間
 ③単純な癒しの時間

デモイメージ(画質が粗くすみません)
React-App-Google-Chrome-2020-03-29-17-13-05.gif

#3、React/Reduxの大まかな流れ
React/Reduxは基本的に下図の流れでデータの受け渡しが進む。
①Reactで作ったComponentでデータの入力 →②ActionCreatorによって入力内容に応じたActionが発動→③Reducerが受け取ったaction Typeに応じてstoreのstateの値が更新される→④更新されたstateの値がComponentに表示される
※厳密にはこんな単純な説明ではないと思いますが、ざっくりこのように理解しています。
この際データベース(今回の場合はFirebase)とのやり取りは、ActionCreatorで行うようにする

image.png

#4、アプリの構成
 ログイン画面、サインアップ画面、メイン画面(データ入力・表示)を作成
 React-routerにより画面に表示されるComponentを切り替えて遷移

App.js
function App() {
  return (
    <BrowserRouter>
      <Box>
        <Container maxWidth="sm">
          <div className="App">
            <NavBar />
            <Switch>
              <Route exact path="/" component={Login} />
              <Route path="/main" component={Main} />
              <Route path="/login" exact component={Login} />
              <Route path="/signup" exact component={SignUp} />
            </Switch>
          </div>
        </Container>
      </Box>
    </BrowserRouter>
  );
}

#5、メイン画面の構成
①Counter ComponentでItemListにリストアップされた時間を表示
②Form Componentで、やったこと、カテゴリー、掛かった時間を入力
③ItemList Componentで入力した内容をリスト表示
 ※ItemLIst Component内にFilter Componentを埋め込み、カテゴリによってリストの表示を絞り込む機能を追加

image.png

Main.js
const Main = () => {
  return (
    <Box>
      <Counter />
      <Form />
      <ItemList />
    </Box>
  );
};

#6、各Componentの構成 (1)Form Component

①:React Hooks を使ってstate(状態)の値を維持・更新できるようにする
 useStateのカッコ内は初期値となる

Form.jsx
const Form = props => {
  const [text, setText] = useState("");
  const [hour, setHour] = useState(0);
  const [status, setStatus] = useState("投資");

②:後に出てくる入力ボタンを押した際に発動する関数
 下で入力するstatus,text,hourの値をaddItem関数を使いprops経由で受け渡し、
 setXXXで画面上の値をリセットする

Form.jsx
  const onClickButton = () => {
    if ((text, hour, status)) {
      props.addItem(status, text, hour);

      setText("");
      setHour(0);
      setStatus("投資");
    }
  };

③:入力フォーム
 ここで入力・選択したstatus,text,hourの値が、ボタンを押した際にデータとして受け渡される

Form.jsx

  return (
    <Box color="text.primary">
      <Container>
        <h3>今日の積み上げ</h3>
        <div className="text-field">
          <Input
            variant="outlined"
            className="input-text"
            type="text"
            value={text}
            onChange={e => {
              setText(e.target.value);
            }}
            placeholder="please input your activity"
          />
          <select
            name="chooseStatus"
            className="radio-select"
            value={status}
            onChange={e => {
              setStatus(e.target.value);
            }}
          >
            <option value="投資">投資</option>
            <option value="浪費">浪費</option>
            <option value="癒し">癒し</option>
          </select>

          <input
            variant="outlined"
            label="Hour"
            className="input-hour"
            type="number"
            value={hour}
            onChange={e => {
              setHour(e.target.value);
            }}
            placeholder="please input hour"
          />
          <p className="hour">Hour</p>

          <Button
            variant="contained"
            color="primary"
            onClick={onClickButton}
            className="button"
          >
            Go
          </Button>
        </div>
      </Container>
    </Box>
  );
};

const mapStateToProps = state => {
  return {
    auth: state.firebase.auth
  };
};

const mapDispatchToProps = dispatch => {
  return {
    addItem: (status, text, hour) => {
      const action = addItem(status, text, hour);
      dispatch(action);
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Form);

#7、各Componentの構成 (2)ItemList Component
①:条件によってClassNameを変える *classnamesライブラリを使用
 入力したstatus(投資、浪費、癒し)によってCSSで色を変える方法を探していたところ、classnamesライブラリというものを発見したので、status応じてclassNameを切り替える設定をした

ItemList.jsx
const ItemList = props => {
  const itemLists = props.items.map((item, index) => {
    const classNameForListItem = ClassNames(
      {
        invested: item.status === "投資"
      },
      {
        expensed: item.status === "浪費"
      },
      {
        rested: item.status === "癒し"
      }
    );

②:入力した値がリスト表示されるようリストの表示内容を設定
 リストを削除するためのボタンとdeleteItem関数を設定
 また、mapメソッド(上から続いている)によりprops経由で渡ってきた値をリストの項目として設定
 あと、リスト項目を作成する場合、固有のKeyを設定しリスト毎を個別管理できるようにする必要
 があるため、ここでは Key=item.idを設定(今回のアプリではFirebaseのドキュメント IDとリンクしている)

ItemList.jsx
    return (
      <Container key={index} maxWidth="sm">
        <li key={item.id} className={classNameForListItem}>
          <span className="item-status">{item.status}</span>
          <span className="item-text">{item.text}</span>
          <span className="item-hour">{item.hour} Hour</span>
          <button
            className="item-button"
            onClick={() => props.deleteItem(item.id)}
          >
            X
          </button>
        </li>
      </Container>
    );
  });

③:itemListsに格納されたli要素をulタグ内で表示、Filter Componentによりfilter機能を盛り込む

ItemList.jsx
  return (
    <Container maxWidth="sm">
      <div className="item-box">
        <h4>積み上げ履歴</h4>
        <Filter />
        <ul className="itemContainer">{itemLists}</ul>
      </div>
    </Container>
  );
};

④:Filter Component
 "全て"、"投資"、"浪費"、"癒し"のボタンを設定
 ボタンを押すと、それぞれshowAll,showInvest,showExpense,showHealing関数が実行される

Filter.jsx
const Filter = props => {
  return (
    <Box color="text.primary">
      <Container>
        <div className="container Filter-container">
          <Button
            variant="outlined"
            className="showAll"
            onClick={props.showAll}
          >
            全て
          </Button>

     ~以下繰り返しのため略

        </div>
      </Container>
    </Box>
  );
};

#8、各Componentの構成 (3)Counter Component
投資、浪費の合計値が表示されるようJSのfilterメソッド、reduceメソッドを使用し計算

Counter.jsx

const Counter = props => {
  const investLists = props.items
    .filter(item => item.status === "投資")
    .map(item => {
      return Number(item.hour);
    });

  const expenseLists = props.items
    .filter(item => item.status === "浪費")
    .map(item => {
      return Number(item.hour);
    });

  const invest = investLists.reduce((acc, amount) => acc + amount, 0);
  const expense = expenseLists.reduce((acc, amount) => acc + amount, 0);

  return (
    <Box>
      <Container maxWidth="sm">
        <h2>積み上げカウンター</h2>
        <div id="displayInevstExpense">
          <div id="invest-field">
            <h4>
              投資 : {invest}
              <span> Hour</span>
            </h4>
          </div>
          <div id="expense-field">
            <h4>
              浪費 : {expense}
              <span> Hour</span>
            </h4>
          </div>
        </div>
      </Container>
    </Box>
  );
};

#9、Action Creatorの構成
①:itemActionCreator
 Form Componentで入力したデータの処理と、ItemList Componentの削除処理をここで記述する
 ここでFirebaseとの連携を行いデータベース機能であるFirestoreを使用
 具体的なFirebaseの使い方については、FrontHacks講師であるつよぽんさんの動画(Firebase入門)で
 学習(FrontHacksとは別教材)
 Firebaseの具体的な連携方法はここでは解説しません。ぜひ動画をご参照いただければと思います。

itemActionCreator.js
export const addItem = (status, text, hour) => {
  return async dispatch => {
    try {
      const db = await firebase.firestore();
      db.collection("activities").add({
        status,
        text,
        hour
      });
      dispatch({
        type: ADD_ITEM,
        status,
        text,
        hour
      });
    } catch (err) {
      dispatch({ type: ADD_ITEM_ERROR, err });
    }
  };
};
export const deleteItem = id => {
  return async dispatch => {
    try {
      const db = await firebase.firestore();
      db.collection("activities")
        .doc(id)
        .delete();

      dispatch({ type: DELETE_ITEM, id });
    } catch (error) {
      dispatch({ type: DELETE_ITEM_ERROR, error });
      alert("delete,NG!!!");
    }
  };
};

②:visibleFilterCreator
 Filter Componentのボタンに対応したアクションを設定
 Firebaseとの連携をリアルタイムに行うためにonSnapshotメソッドを使う
 詳細は上述の動画もしくは公式ドキュメントを参照ください
 Cloud Firestore でリアルタイム アップデートを入手する
 showAll,showInvest,showExpense,showHealingでほぼ同じ処理なので省略します

visibleFilterCreator.js
export const showAll = payload => {
  return async dispatch => {
    try {
      const db = await firebase.firestore();
      await db.collection("activities").onSnapshot(querySnapshot => {
        const refAll = querySnapshot.docs.map(doc => {
          return {
            ...doc.data(),
            id: doc.id
          };
        });
        dispatch({
          type: SHOW_ALL,
          payload: refAll
        });
      });
    } catch (error) {
      dispatch({ type: "SHOW_ALL_ERROR", error });
      alert("NG");
    }
  };
};
       ~以下略

③:authActionCreator
 ユーザー登録とログインに使う処理を記載
 Firebaseの機能のうちAuthenticationを使用して実装
 今回はメールアドレスを使ったログイン方法です
 具体的な実装方法は、Reducerの処理含め下記の記事を参考にさせてもらいました

 参考: React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発

authActionCreator.js
export const signIn = (email, password) => {
  return async (dispatch, getState, { getFirebase }) => {
    try {
      const firebase = await getFirebase();
      firebase.auth().signInWithEmailAndPassword(email, password);
      dispatch({ type: "LOGIN_SUCCESS" }, email, password);
    } catch (err) {
      dispatch({ type: "LOGIN_ERROR" }, err);
    }
  };
};

export const signOut = () => {
  return async (dispatch, getState, { getFirebase }) => {
    try {
      const firebase = await getFirebase();
      await firebase.auth().signOut();
      dispatch({ type: "SIGNOUT_SUCCESS" });
    } catch (err) {
      dispatch({ type: "SIGNOUT_ERROR" }, err);
    }
  };
};

export const signUp = (email, password, firstName, lastName) => {
  return async (dispatch, getState, { getFirebase, getFirestore }) => {
    try {
      const firebase = await getFirebase();
      firebase.auth().createUserWithEmailAndPassword(email, password);
      dispatch({ type: "SIGNUP_SUCCESS" });
    } catch (err) {
      dispatch({ type: "SIGNUP_ERROR", err });
    }
  };
};

#10、Reducerの構成
 ①itemReducer
 itemActionCreatorでdispatchされたアクションのTypeに基づいてstateの値を更新する処理を行う

itemReducer.js
export const itemReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_ITEM:
      const item = new Item(action.id, action.status, action.text, action.hour);
      return [...state, item];
    case ADD_ITEM_ERROR:
      return state;
    case DELETE_ITEM:
      return state.filter((item, id) => {
        return action.id !== item.id;
      });

    case DELETE_ITEM_ERROR:
      return state;
    default:
      return state;
  }
};

 ②visibleFilterReducer
 visibleFilterCreatorでdispatchされたアクションのTypeに基づいてstateの値を更新する処理を行う
 詳細コードは省略(もし興味があればGithubをご覧ください)

 ③authReducer
 authActionCreatorでdispatchされたアクションのTypeに基づいてstateの値を更新する処理を行う
 詳細コードは省略(もし興味があればGithubをご覧ください)

 ④rootReducer
 3つのreducerがあるので、combineReducersを使って一本化する

rootReducer
import { itemReducer } from "./itemReducer";
import { authReducer } from "./authReducer";
import { visibleFilterReducer } from "./visibleFilterReducer";
import { combineReducers } from "redux";


const rootReducer = combineReducers({
  itemInfo: itemReducer,
  visibleFilter: visibleFilterReducer,
  auth: authReducer
});

export default rootReducer;

#11、storeの構成
 reduxのcreateStoreメソッドでstoreを作る
 また、Firebaseとの連携で非同期処理が必要となるため、非同期処理に必要なredux-thunkとapplyMiddlewareを使用し、10で一本化したrootReducerと合わせて設定する

store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "../reducers/rootReducer";
import thunk from "redux-thunk";
import { reduxFirestore, getFirestore } from "redux-firestore";
import { reactReduxFirebase, getFirebase } from "react-redux-firebase";
import fbConfig from "./../Config/fbConfig";

const store = createStore(
  rootReducer,
  compose(
    applyMiddleware(thunk.withExtraArgument({ getFirebase, getFirestore })),
    reactReduxFirebase(fbConfig, { attachAuthIsReady: true }),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )
);

export default store;

#12、苦労したこと、身についたこと
 ・文法的なエラーから、タイポ的なエラー、importミスからくるエラーなど、あらゆるエラー
  により時間が奪われ、想定以上に時間がかかった
  →エラー対応の中で、まずはエラー文をシンプルに受け止めることが大事だとわかった。
  また、英語文献(stack overflow)やQiitaの記事、Githubのコードを参考にすることで、
  エラーの対応方法やそもそもの言語に対する理解も深まった
  苦労してエラーを解決して、自分の思った通りに動いた時の快感を覚えたことで、
  あまりめげなくなった
  Console.logと少し友達になれたおかげで、データの流れが分かるようになった
 ・React/Reduxだけで実装した際には、それぞれの機能の役割の理解が不十分でアプリ作成が
  進まなかった
  →教材を何度も見返したり、ノートにデータフローを書き出して、それぞれのつながりを何度も
  確認することで理解を深めることができた。
 ・データベース(Firebase)と連携をさせた際に、非同期処理の理解が不十分でデータのやり取りが
  スムーズに出来ない、画面の表示がリアルタイムに更新されない、データベースから特定の
  データを削除するための固有のidの取得方法が分からない、というところで苦労した
  →教材を見返す、公式ドキュメントを読む、Youtubeの海外動画を見まくる、
  などで関連した情報を集約して実装したら、何とか動くコードが書けるようになった
  とにかくまずは動くコードを書くという姿勢が身に付いた
 ・Filter機能(特定条件で絞り込む)を実装できるようになれば今後応用が利くと思い、
  実装にこだわった。
  →データベースからうまく配列を作り直して、JSのfilterメソッドで絞り込むという手法が
  身に付いた
 ・正直、アプリとしては大したものではないが、学習した内容を総復習するという意味
  で作ってよかったし、自分で何かアプリやサービスを作れる事が単純に楽しいと思った
  
#13、今後取り組んでいきたいこと
バックエンドに取り組みたい
 ・Webフレームワークを1つ覚える(現在Expressを学習中)
 ・RDBを身につける(今回のFirebaseのようなNoSQL以外も覚える)
 ・WebフレームワークとRDBを連携したAPIサーバーを実装する
 ・APIサーバーとフロントを連携したWebアプリを作る
テストの方法を覚える
 ・テストコードを理解することで、より効率的な開発を行えるようになる

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?