アドカレには間に合わなかったけど、ReactHooksとFirebaseでTodoアプリ作ったので手順まとめます。
今回作ったもの
コードはこちら
https://github.com/k-tada/react-firebase-todo
使った技術
- React(v16.7.0-alpha.2)
- Hooks使えるバージョン
- Firebase
- Authentication
- Firestore
- Hosting
Firebaseの準備
Firebaseプロジェクトを作る
https://console.firebase.google.com/u/0/?hl=ja
にアクセスし、プロジェクトを追加します。
Authenticationを有効化
左メニューのAuthenticationを選択し、ログイン情報でメール/パスワードを有効にします。
Firestoreの有効化、設定
左メニューのDatabaseを選択し、データベースの作成を選択します。
- データベースを作成
一旦テストモードで作成します。ルールは後で作ります。
- コレクションを追加
コレクションを追加するためにテストデータを1件登録しておきます。
プロジェクトの作成
parcelでちゃちゃっと作るのが楽なので今回もparcel使います。
各種インストール、設定
$ mkdir react-firebase-todo && cd $_
$ npm init -y
$ npm i -S react@next react-dom@next styled-components @material-ui/core firebase
$ npm i -D parcel-bundler
"scripts": {
+ "start": "parcel src/index.html -d public"
},
Lint+Prettierの設定もサクッと用意
$ npm i -D eslint prettier prettier-eslint prettier-eslint-cli babel-eslint
Firebaseの設定
Firebaseの初期化処理と必要なAPIの設定を行います。
認証情報はFirebase ConsoleのAuthenticationの右上にあるウェブ設定からコピペしてください。
import firebase from 'firebase'
firebase.initializeApp({
// 認証情報をここに
})
const auth = firebase.auth()
const db = firebase.firestore()
db.settings({ timestampsInSnapshots: true })
export { auth, db }
認証用のContextを作る
FirebaseのAuthenticationを使って認証用のContextを作成します。
作成するContextの構成はこんな感じ。
const AuthContext = createContext()
const AuthProvider = ({ children }) => {
:
:
}
export { AuthContext, AuthProvider }
このAuthProvider
内に処理を追加していきます。
まずはuseState
で認証済みユーザのローカルステートを作成。
const [currentUser, setCurrentUser] = useState(null)
初回アクセス時にfirebase.auth().onAuthStateChanged()
を使って認証済みかどうかのチェックを行います。
useEffect(() => {
auth.onAuthStateChanged(user => setCurrentUser(user))
}, [])
後はサインアップ、ログイン、ログアウト用のメソッドを用意します。
基本すべて同じ作り方なのでサインアップメソッドだけ載せます。
firebase.auth().createUserWithEmailAndPassword()
でサインアップ後、firebase.auth().onAuthStateChanged()
で認証情報を更新しています。
const signup = async (email, password) => {
await auth.createUserWithEmailAndPassword(email, password)
auth.onAuthStateChanged(user => setCurrentUser(user))
}
後はProviderでラップしたchildrenを返してやればOKです。
return (
<AuthContext.Provider value={{ currentUser, signup, signin, signout }}>
{children}
</AuthContext.Provider>
)
全体像はこんな感じ。なんかもうちょっと上手く書く方法あるかもしれないけど今は(゚ε゚)キニシナイ!!
import React, { createContext, useState, useEffect } from 'react'
import { auth } from '../utils/firebase'
const AuthContext = createContext()
const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
const signup = useCallback(async (email, password) => {
await auth.createUserWithEmailAndPassword(email, password)
// auth.onAuthStateChanged(user => setCurrentUser(user))
}, [])
const signin = useCallback(async (email, password) => {
await auth.signInWithEmailAndPassword(email, password)
// auth.onAuthStateChanged(user => setCurrentUser(user))
}, [])
const signout = useCallback(async () => {
await auth.signOut()
// auth.onAuthStateChanged(user => setCurrentUser(user))
}, [])
useEffect(() => {
auth.onAuthStateChanged(user => setCurrentUser(user))
}, [])
return (
<AuthContext.Provider value={{ currentUser, signup, signin, signout }}>
{children}
</AuthContext.Provider>
)
}
export { AuthContext, AuthProvider }
Todo用のContextを作る
FirebaseのFirestoreと接続してTodoの取得、追加、更新、削除を行うContextを作ります。
まずは取得。
firebase.firestore().collection(name).onSnapshot()
を使ってリアルタイムに更新されるようにします。
ついでに、firebase.firestore().collection(name)
までの部分を変数に括りだすためにまとめてuseMemo
で処理します。
AuthContext
で管理しているcurrentUser
のuid
を使って、自分のTodoのみを取得するようにしています。更新、削除用にドキュメントIDを添えてデータを保存しています。
const [todos, setTodos] = useState([])
const { currentUser } = useContext(AuthContext)
const collection = useMemo(() => {
const col = db.collection('todos')
// 更新イベント監視
col.where('uid', '==', currentUser.uid).onSnapshot(query => {
const data = []
query.forEach(d => data.push({ ...d.data(), docId: d.id }))
setTodos(data)
})
return col
}, [])
Todoの追加、更新、削除処理は↑のuseMemo
で取得したcollection
を対象に行います。
追加処理はこんな感じ。
const add = useCallback(async text => {
await collection.add({
uid: currentUser.uid,
text,
isComplete: false,
createdAt: new Date(),
})
}, [])
後はAuthContextと同様にProviderでラップしてやればOK。
全体像はこんな感じ。
import React, {
createContext,
useContext,
useState,
useMemo,
useCallback,
} from 'react'
import { AuthContext } from './auth'
import { db } from '../utils/firebase'
const TodosContext = createContext()
const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([])
const { currentUser } = useContext(AuthContext)
const collection = useMemo(() => {
const col = db.collection('todos')
// 更新イベント監視
col.where('uid', '==', currentUser.uid).onSnapshot(query => {
const data = []
query.forEach(d => data.push({ ...d.data(), docId: d.id }))
setTodos(data)
})
return col
}, [])
const add = useCallback(async text => {
await collection.add({
uid: currentUser.uid,
text,
isComplete: false,
createdAt: new Date(),
})
}, [])
const update = useCallback(async ({ docId, text, isComplete }) => {
const updateTo = {
...todos.find(t => t.docId === docId),
text,
isComplete,
}
if (isComplete) {
updateTo.completedAt = new Date()
}
await collection.doc(docId).set(updateTo)
}, [todos])
const remove = useCallback(async ({ docId }) => {
await collection.doc(docId).delete()
}, [todos])
return (
<TodosContext.Provider value={{ todos, add, update, remove }}>
{children}
</TodosContext.Provider>
)
}
export { TodosContext, TodosProvider }
Contextを使って残りのReactを作る
未認証の場合はログイン画面を表示させる
今回はreact-routerとかを使うのがめんどくさかったので自前で超簡易的なルーティング処理を作ってます。
まずはルートコンポーネントでAuthProviderでラップする。
export default () => (
<AuthProvider>
<Main>
<Router
renderTodos={() => <Todos />}
renderLogin={() => <Login />}
/>
</Main>
</AuthProvider>
)
Routerコンポーネントで認証状態でrenderするコンポーネントを変えてやる。
export default ({ renderLogin, renderTodos }) => {
const { currentUser } = useContext(AuthContext)
return (
<Fragment>
{currentUser ? renderTodos() : renderLogin()}
</Fragment>
)
}
後はLoginコンポーネントとTodosコンポーネントを作ればOK。
この辺のコードは割愛。
Contextの値、メソッドを使いたいときは下記のようにuseContext
でContextから抽出して使えばOK。
const { signin } = useContext(AuthContext)
Firestoreルールの設定
FirebaseコンソールからFirestoreを作成した際はテストルールを選んでいたので基本的にはすべての読み取り書き取りが許可される状態になっています。
次項のホスティングを行う前にこの辺をちゃんと修正してやりましょう。
今回はコレクションがtodos
のみで、各todoドキュメントのuidでどのユーザのTodoなのかを管理するような設計にしているので、以下のようなルールを設定します。
- 読み取りは一旦全部許可
- 新規作成は認証済みユーザのみ許可
- 更新、削除は対象ドキュメントの作成ユーザのみ許可
上記ルールをFirestoreに設定する場合の定義は下記の様になります。
多分読めばわかるので説明はばっさりカット。
service cloud.firestore {
match /databases/{database}/documents {
match /todos/{todo} {
function isSignedIn() {
return request.auth != null;
}
function isAuthor() {
return request.auth.uid == resource.data.uid;
}
allow read;
allow create: if isSignedIn()
allow update, delete: if isAuthor();
}
}
}
この設定をFirebaseコンソールのDatabase→ルールに記載してやればOK。
ホスティング
コードが完成したらFirebaseのHostingを使ってホスティングしましょう。
今回はCLIを使ってホスティングします。
Firebase CLIのインストール
npm i -g firebase-tools
ndenv rehash # ndenv使ってる人だけ
ホスティィィング
firebase login
# ログイン
firebase init
# Authentication, Firestore, Hostingを選択して後は適宜選択してinit
firebase deploy
あとがき
最近何かと話題のReactHooksを使ってFirebaseとの連携をしてみました。
認証処理、データ処理なんかをContextにまとめることで、Reduxなどのmiddlewareを使わなくても見通しのいいコードを書くことが出来たと思います。
Contextのおかげでpropsのバケツリレーが無いのもいいですね。
個人的にはContext + Hooksでビジネスロジック周りの処理をまとめる書き方結構気に入ったので簡単なサービスを開発するときはこの方針でやってやろうと思ってます。
あとFirebaseが噂に聞いていた通り非常に便利だったので、もう少し使い込んでみたいと思いました(小並感