前回の投稿から期間が空いてしまいましたが、引き続きミニアプリを作っていきます。
前回の投稿はこちらから。
React + Firebaseのミニアプリを作る【実装編1】
React + Firebaseでミニアプリを作っていきます。 前回の記事で作るアプリの計画をしたので今回は実際にコードを書いていきます。 前回の記事はこちらから↓ Con ...
続きを見る
どんなアプリを作るかは【計画編】を見て下さい。
今回はCRUDの実装を開始します。
Contents
CRUD実装前に...
早速コードを書いていきたいのですが、どうしても触れなければならないFirebaseの使い方について書いておきます。
Firebaseについて知っている方・すでに使ったことがある方は読み飛ばして下さい。
REST API と Firebase SDK
このアプリではバックエンドでFirebaseを使うのですが、Firebaseはかなり高機能で何をするにしても多くの選択肢があります。
データベースはRealtime DatabaseかCloud Firestoreの2択がありますが、今回はRealtime Databaseを選択しました。
さらにFirebaseへのアクセスに関して、React + Firebase Realtime Databaseでアプリを作る際には大きく分けるとREST APIかFirebase SDKを使用するかの2択があります。
REST API
メリット
いわゆる一般的なREST APIと同じようにFirebaseのDBにアクセスが可能で、特別な設定なく使えるのが良いところです。
例えばNode.js(や他サーバー)への移行(もしくはその逆)を考えている場合は有効です。
デメリット
Firebaseの他のサービスとの連携で面倒になることがあります。特に今回のアプリでは画像アップロードが必要になり、Cloud Storageというサービスを使います。
Cloud Storageでも認証が必要になるのですが、REST側での認証とCloud Storage側での認証が上手く連携がとれません。こういったデメリットがあります。
参考
REST APIでのデータの保存↓
認証関連↓
Firebase SDK
メリット
ざっくり言うとFirebaseにはFirebaseの機能を簡単に使うための仕組みがあらかじめ用意されており、それがSDKです。
認証・ DB操作・ファイルアップロードなどFirebaseの機能が横断的に簡単に扱うことが出来ます。
firebaseのパッケージをインストールすることで、使用可能になります。
デメリット
FirebaseオリジナルのSDKを使うため、移行の際は書き換えが必要。
多少は学習コストがかかることもデメリットになります。
参考
SDKでのデータ読み書き↓
認証↓
結論
Firebaseを使うなら基本的にはFirebase SDKを使ったほうがいいですが、今回はどちらも使います。
というのもバックエンドにFirebaseを使っている人以外には参考にならなくなってしまうのが嫌なのでREST APIでも作成することに決めました。(そのせいで時間はかかってしまいましたが......)
全体の流れは以下のようになります。
- REST APIでCRUD作成
- Auth REST APIで認証機能を追加
- Cloud Storageで画像アップロード
- 1, 2をFirebase SDKで書き換え
- 細かな修正とデプロイ
まずはREST APIで作りますが、画像アップロードの際に認証の問題でうまく対処できなくなります。
その後SDKで全ての処理を書き換え、認証問題を対応していきます。
Firebaseを使っている人にも、もう少し一般的なWebアプリ作成を眺めたい人にも(たぶん)参考になるように書いていくつもりです。
データ構造を決める
書き始める前にデータ構造を決めておきます。
必要になるデータは「お題」に関するデータと「質問&答え」のデータです。
必要になるデータ | データの中身 | 命名 |
お題 | タイトル・画像情報 | Subject |
質問&答え | 質問内容、答え | QuestionAnswer (qa) |
(*QuestionAnswerは長いのでqaと略す場合があります。)
ポイント
今回使用するFirebaseのRealtime DatabaseはNoSQLデータベースで、JSONを保存するようなイメージで使います。
上の表を踏まえて、今回は以下のようなデータ構造にします。
{ "userId" : { "subject" : { "subjectId" : { "image" : "https://firebasestorage.....", "imageName" : "2020-03-06T10:33:57.657Zreact-image.png", "title" : "React" } }, "qa" : { "subjectId" : { "a1" : "", "a2" : "", "a3" : "", "a4" : "", "a5" : "", "q1" : "「それ」を一言で表すと?", "q2" : "「それ」はどんな問題をどう解決した?", "q3" : "「それ」の代わりとなるものは?", "q4" : "「それ」に関連するキーワードは?", "q5" : "「それ」の目次を作るとしたら?" } } } }
ポイントは「userIdの下に各データがある」という点と「qaのデータはsubjectのIDがキーになっている」という点です。
JSONを深い階層にはしたくないのですが(参考)、認証のことを考えるとuserId以下の階層にデータを入れると制限をかけやすいです。
補足
userIdの下にデータを入れることで、userIdと合致する認証データを持っている場合のみデータに対してアクセス可能にします。
なぜこのデータ構造なのか
今回のアプリでは「Subjectをリスト表示をするページ」と「QAを表示する詳細ページ」の2画面が主な機能になりますが、それぞれについてデータを取り出しやすい形にしてあります。
例えば、リスト形式で表示する際には/userId/subject
以下の全データを取り出せばいいですし、詳細を表示する際には/userId/qa/subjectId/
以下のデータから全ての「質問と答え」を取得します。
このようにDBアクセスをする際に取り出しやすい形で設計をしました。
今回はこのデータ形式でアプリを作っていきます。ただし認証を実装する前はuserIdの階層は無視して作成するので注意して下さい。
REST APIでCRUD作成
今回ベースとするソースコードは以下のコードになります。前回の最終コードに若干の修正を加えていますが、大きな変化はありません。(コンポーネントの分割に修正をしただけです。)
実際に動かしたい場合はzipファイルでダウンロードしてyarn install
して下さい。
CRUDの完成コードはこちら↓
フォルダ構成やファイルの全体像がわからなくなった場合は完成コードを参照して下さい。
準備
必要になるパッケージをインストールしておきます。
状態管理のためにReduxを、副作用の対応にはredux-thunkを、非同期通信にはaxiosを使用します。
yarn add redux react-redux redux-thunk axios
Reduxやredux-thunkの基本的な解説はしませんが、セットアップのソースコードは載せておきます。特別なことはしていないので、必要ない方は読み飛ばして下さい。
Reduxの準備(読み飛ばし可)
Storeを作成
src > store > index.jsに次のように記載します。
import { createStore, applyMiddleware, compose } from "redux"; import thunk from "redux-thunk"; import rootReducer from "../reducers/index"; const composeEnhancers = process.env.NODE_ENV === "development" ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : null || compose; const store = createStore( rootReducer, composeEnhancers(applyMiddleware(thunk)) ); export default store;
デバッグでRedux DevToolsを使用するため、その設定をしています。process.env.NODE_ENVの条件文によって開発時のみRedux DevToolsが使用可能になるようにしています。
Reducerの準備
Reducerはsrc > reducers > index.jsで以下のように設定。
import { combineReducers } from "redux"; import subjects from "./subjects"; const rootReducer = combineReducers({ subjects }); export default rootReducer;
subjectsは今後作成するreducerです。このようにcombineReducersに作成したreducerを渡していくことで複数のreducerをrootReducerにまとめます。
それが先程のstore/index.jsでのrootReducerになります。
Providerの設定
App.jsのコンポーネント部分を以下のようにProviderでラップし、先程exportしたstoreを指定します。
// 各種importも忘れずに // import { Provider } from "react-redux"; // import store from "./store/index"; <Provider store={store}> <ThemeProvider theme={darkTheme}> <CssBaseline /> <Router> <Header /> <Content /> </Router> </ThemeProvider> </Provider>
Reducerの原型
機能を追加するたびにReducerは増えていきますが、各Reducerの原型は以下の通りです。
import * as ACTION_TYPES from "../actions/actionTypes"; const initialState = { subjectList: [], subject: {}, questions: {}, answers: {} }; const subjects = (state = initialState, action) => { switch (action.type) { /* 例 case ACTION_TYPES.CREATE_SUBJECT: return { ...state, subjectList: state.subjectList.concat(action.payload) }; */ default: return state; } }; export default subjects;
ActionTypes
src > actions > actionTypes.jsにAction Typesを変数として定義しておいて下さい。
例えば以下のように追加していきます。
// Subject export const CREATE_SUBJECT = "CREATE_SUBJECT"; export const READ_SUBJECT = "READ_SUBJECT"; export const UPDATE_SUBJECT = "UPDATE_SUBJECT"; export const DELETE_SUBJECT = "DELETE_SUBJECT"; // General export const API_FAILED = "API_FAILED";
以上でReduxの準備は完了です。
axios
今回axiosはインスタンスを生成して使用します。
インスタンスを生成することでaxiosがちょっとだけ便利に扱えます。
例えばbaseURLを指定することで、axios.get("https://xxx.firebaseio.com/subject.json")
のように毎回URL全てを指定する必要がなく、axios.get("/subject.json")
のように簡略化できます。
これはインスタンスを生成しなくても使える機能ですが、複数の通信先があるときには別々にインスタンスを生成し設定しておくことが出来るので便利になります。
特にFirebaseでは認証のURLは別で用意されているので、DB用のインスタンス・認証用のインスタンスと2つ用意しておきます。
以下が一例です。インスタンスを生成するにはaxios.createするだけ。
import axios from "axios"; const instance = axios.create({ baseURL: "URLはFirebaseで確認して下さい" // timeout: 2000 }); // リクエストのインターセプター instance.interceptors.request.use( config => { // リクエストが送られる「前に」実行される // 例えば、トークンを設定する // config.headers.common["Authorization"] = "AUTH_TOKEN"; return config; }, error => { // then/catchの処理の「前に」実行されるエラーハンドリング console.log("=== Request Failed ==="); return Promise.reject(error); } ); // レスポンスのインタセプター // then/catchの処理の「前に」実行される instance.interceptors.response.use( response => { console.log("=== Success ==="); return response; }, error => { console.log("=== Response Error ==="); return Promise.reject(error); } ); export default instance;
baseURLは画像を参考にFirebaseのプロジェクトのURLを確認しておいて下さい。
インターセプターは「通信前」「通信後」などのタイミングで共通の処理を走らせる場合に役に立ちます。
例えば通信前に認証データを追加したり、認証の期限が切れていた場合に再試行したりと上手く使えば便利な機能です。
それでは本格的にCRUD処理を書いていきます。
READ
まずはデータの読み込み(READ)から書いていきます。一番簡単なので最初に書きます。
コンポーネント作成→Action Creatorの作成→Reducerの作成と進めます。
コンポーネント
ListView(一覧画面)が表示されるときにデータを取得したいのでuseEffectを使います。
以下のようにListViewを設定します。
import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Link } from "react-router-dom"; import { makeStyles } from "@material-ui/core/styles"; import Grid from "@material-ui/core/Grid"; import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import MediaCard from "./list/MediaCard"; import { readSubject } from "../../actions/subjects"; const useStyles = makeStyles(theme => ({ link: { textDecoration: "none" } })); const ListView = props => { const classes = useStyles(); const dispatch = useDispatch(); const items = useSelector(state => state.subjects.subjectList); useEffect(() => { dispatch(readSubject()); }, [dispatch]); return ( <React.Fragment> <Grid item xs={12}> <Box textAlign="center"> <Link to="/create" className={classes.link}> <Button variant="contained" color="primary"> Create </Button> </Link> </Box> </Grid> {items.map(item => ( <Grid item key={item.id}> <Link to={`/detail/${item.id}`} className={classes.link}> <MediaCard title={item.title} /> </Link> </Grid> ))} </React.Fragment> ); }; export default ListView;
注意ポイント
前回の記事とはitems.map(...の箇所も含め、いくつか変更点があるのでご注意下さい。(ダミーデータからデータの形が変わったため)
クラスベースで作成する場合はcomponentDidMountで非同期通信やaction creatorの呼び出しを行いますが、関数形式で書く場合はuseEffect内で行います。
少し前はReactとReduxを連携させるためにmapStateToPropsやmapDispatchToPropsを書く必要があったのですが、今はuseSelector・useDispatchを使うことで簡単にstateへのアクセスやaction creatorのdispatchが可能になっています。
補足
useEffectはuseEffect(() => { ... } , [dispatch])
のように第2引数が指定されています。ここではdispatchを指定していますが、これは深く考えずuseEffect(() => { ... } , [])
と同じと考えて下さい。
useEffectを詳しく説明すると本題からそれるので公式ドキュメントを参考にして下さい。
大事なところだけ抜粋↓
空の配列 [] を渡すと、この副作用がコンポーネント内のどの値にも依存していないということを React に伝えることになります。つまり副作用はマウント時に実行されアンマウント時にクリーンアップされますが、更新時には実行されないようになります。
空の配列を第2引数に渡すことで、マウント時のみ実行されます。要はcomponentDidMount的な使い方が出来るということです。
Action Creator
次にコンポーネントのuseEffectで呼び出されるaction creatorを作成します。
やることは
- GETメソッドでデータの取得
- 通信結果をstateに反映するためactionを生成
これだけです。
actions > subjects.jsは以下の通り。
import * as ACTION_TYPES from "../actions/actionTypes"; import { apiFailed } from "./index"; import axios from "../axios"; // src > actions > subjects.js内 export const readSubject = () => async dispatch => { try { const result = await axios.get("/subject.json"); /* result: { data : { id: { title: "", image: "" } } } の形を下の形式に変更 {id: "", title: "", image: ""} */ const subjectList = Object.keys(result.data).map(id => ({ ...result.data[id], id: id })); dispatch(readSubjectSuccess(subjectList)); } catch (error) { dispatch(apiFailed("read subject failed")); } };
これでFirebaseにデータがあれば、データを取得できます。DB側ではsubjectのidが Keyになっているためデータ形式を変更しています。(コード内のコメントを参照)
通信が成功したらreadSubjectSuccessをdispatchしています。
// 同じくactions > subjects.js内 const readSubjectSuccess = subjectList => { return { type: ACTION_TYPES.READ_SUBJECT, payload: subjectList }; };
後はこれをReducerで処理するだけです。
ポイント
api通信の失敗時にapiFailedを呼び出していますが、現段階では特に処理はしていません。例↓
// actions > index.js import * as ACTION_TYPES from "./actionTypes"; export const apiFailed = message => { return { type: ACTION_TYPES.API_FAILED, payload: { message: message } }; };
Reducer
reducer側では取得したデータ(を配列に変換したもの)をそのままstateに反映します。
src > reducers > subjects.jsを作成し、以下のようにします。
import * as ACTION_TYPES from "../actions/actionTypes"; const initialState = { subjectList: [], subject: {}, questions: {}, answers: {} }; const subjects = (state = initialState, action) => { switch (action.type) { case ACTION_TYPES.READ_SUBJECT: return { ...state, subjectList: action.payload, questions: {}, answers: {} }; default: return state; } }; export default subjects;
subjectListに通信で得たデータを格納します。
(現段階ではsubjects, questions, answersは気にしなくて大丈夫です。詳細画面用なので。)
確認
以下のデータをFirebaseのRealtime DBにインポートしてみてください。
{ "qa" : { "-M1QdwcNt61PPl4tS7hh" : { "a1" : "aabbb", "a2" : "hogehoge", "a3" : "dddd", "a4" : "hogehoge", "a5" : "bbdd\nこれはabcdef", "q1" : "「React」を一言で表すと?", "q2" : "Reactはどんな問題をどう解決した?", "q3" : "Reactの代わりとなるものは?", "q4" : "Reactに関連するキーワード", "q5" : "Reactの目次を作るとしたら?" }, "-M1tie57176pPa4kW-rd" : { "a1" : "", "a2" : "", "a3" : "", "a4" : "", "a5" : "", "q1" : "「それ」を一言で表すと?", "q2" : "「それ」はどんな問題をどう解決した?", "q3" : "「それ」の代わりとなるものは?", "q4" : "「それ」に関連するキーワードは?", "q5" : "「それ」の目次を作るとしたら?" } }, "subject" : { "-M1QdwcNt61PPl4tS7hh" : { "image" : "", "title" : "React & Redux" }, "-M1tie57176pPa4kW-rd" : { "image" : "", "title" : "JavaScript" } } }
yarn start
でプログラムを実行すると以下のように表示されるはずです。
これでSubjectに関してはサーバーからのデータを表示することが出来ました。
===
かなり長くなってしまったのでここで一旦区切ります😔
続きは明日までに投稿します。
更新しました↓
React + Firebaseのミニアプリを作る【実装編3】
前回に続きReact+Firebaseでミニアプリを作っていきます。前回の投稿はこちらから。 どんなアプリを作るかは【計画編】を見て下さい。 前回はReduxのセットアップと、Fireb ...
続きを見る