#はじめに
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、アプリ概要
その日その日にやったことをそれぞれカテゴライズしてカウントしていくというシンプルなアプリ
(ログイン機能・データベース連携あり)
<カテゴリ>
①自己投資した時間
②浪費した時間
③単純な癒しの時間
#3、React/Reduxの大まかな流れ
React/Reduxは基本的に下図の流れでデータの受け渡しが進む。
①Reactで作ったComponentでデータの入力 →②ActionCreatorによって入力内容に応じたActionが発動→③Reducerが受け取ったaction Typeに応じてstoreのstateの値が更新される→④更新されたstateの値がComponentに表示される
※厳密にはこんな単純な説明ではないと思いますが、ざっくりこのように理解しています。
この際データベース(今回の場合はFirebase)とのやり取りは、ActionCreatorで行うようにする
#4、アプリの構成
ログイン画面、サインアップ画面、メイン画面(データ入力・表示)を作成
React-routerにより画面に表示されるComponentを切り替えて遷移
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を埋め込み、カテゴリによってリストの表示を絞り込む機能を追加
const Main = () => {
return (
<Box>
<Counter />
<Form />
<ItemList />
</Box>
);
};
#6、各Componentの構成 (1)Form Component
①:React Hooks を使ってstate(状態)の値を維持・更新できるようにする
useStateのカッコ内は初期値となる
const Form = props => {
const [text, setText] = useState("");
const [hour, setHour] = useState(0);
const [status, setStatus] = useState("投資");
②:後に出てくる入力ボタンを押した際に発動する関数
下で入力するstatus,text,hourの値をaddItem関数を使いprops経由で受け渡し、
setXXXで画面上の値をリセットする
const onClickButton = () => {
if ((text, hour, status)) {
props.addItem(status, text, hour);
setText("");
setHour(0);
setStatus("投資");
}
};
③:入力フォーム
ここで入力・選択したstatus,text,hourの値が、ボタンを押した際にデータとして受け渡される
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を切り替える設定をした
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とリンクしている)
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機能を盛り込む
return (
<Container maxWidth="sm">
<div className="item-box">
<h4>積み上げ履歴</h4>
<Filter />
<ul className="itemContainer">{itemLists}</ul>
</div>
</Container>
);
};
④:Filter Component
"全て"、"投資"、"浪費"、"癒し"のボタンを設定
ボタンを押すと、それぞれshowAll,showInvest,showExpense,showHealing関数が実行される
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メソッドを使用し計算
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の具体的な連携方法はここでは解説しません。ぜひ動画をご参照いただければと思います。
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でほぼ同じ処理なので省略します
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 を使ってログイン機能あり掲示板アプリ開発
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の値を更新する処理を行う
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を使って一本化する
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と合わせて設定する
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アプリを作る
テストの方法を覚える
・テストコードを理解することで、より効率的な開発を行えるようになる