- 投稿日:2020-01-23T16:27:39+09:00
Typescript×React×Hooksで会員管理②Contextでアプリの状態管理
前回は Typescript×React×Hooks 及び Firebase Authentication を用いて簡単な会員管理アプリを構築しました。今回はより実践的なアプリケーションとするべく、React Hooks の一部である Context(createContext、useContext)を用いて、アプリケーション全体の状態管理をできるようにしていきます。
全 3 回の内容は下記です。
- Firebase Auth で認証基盤外出し
- Context でアプリの状態管理
- Formik と Yup でフォームバリデーション
利用している技術要素
- Firebase Authentication
- Typescript
- React
- React Hooks
- Context ←NEW
- Material UI
アプリケーション全体の状態管理に関して
React Hooks には Context(コンテクスト)という、アプリ全体で横断的に利用される状態を管理する機構が備わっています。
Context の背景には、典型的な React アプリケーションでは Props バケツリレー地獄になりやすいとか、それを防ぐために Redux が使われていたけど Hooks がそれに置き換わりそう、といった文脈があります。どのようなタイミングでアプリケーション全体の状態管理が必要になるかに関して、下記のような例が挙げられます。
- 言語や国の選択
- ライトモード/ダークモードといった画面テーマの選択
- 文字の大きさの設定
- ログインしているユーザーの情報
ざっくりと、それが切り替わるとアプリケーションの複数の箇所で挙動が変化するなにか、といったイメージです。ログインしているユーザーの情報もまさにそれに当たるので、前回の記事で紹介したサンプルアプリをベースに Context を導入していきます。
ソースコード
前回との差分 を見ると結構わかりやすいと思います。
デモ
アプリケーションの動き自体は前回と同じです。
React アプリのポイント解説
前回との差分中心に説明します。
まず Auth.tsx というファイルを追加し、その中で Context を管理するようにします。
なお、こちらの実装は ReactHooks + Firebase(Authentication, Firestore)で Todo アプリ作る を全面的に参考にさせていただいています。海外の技術記事・Youtube 解説動画など見渡しても、この qiita 記事の実装が一番良かったです。
Auth.tsximport { User } from "firebase"; import React, { createContext, useEffect, useState } from "react"; import auth from "./firebase"; // Contextの型を用意 interface IAuthContext { currentUser: User | null | undefined; } // Contextを宣言。Contextの中身を {currentUser: undefined} と定義 const AuthContext = createContext<IAuthContext>({ currentUser: undefined }); const AuthProvider = (props: any) => { // Contextに持たせるcurrentUserは内部的にはuseStateで管理 const [currentUser, setCurrentUser] = useState<User | null | undefined>( undefined ); useEffect(() => { // Firebase Authのメソッド。ログイン状態が変化すると呼び出される auth.onAuthStateChanged(user => { setCurrentUser(user); }); }, []); return ( <AuthContext.Provider value={{ currentUser: currentUser }} > // こうすることで、下階層のコンポーネントを内包できるようになる {props.children} </AuthContext.Provider> ); }; export { AuthContext, AuthProvider };Typescript なので厳格気味に色々書いていますが、ようするに AuthProvider という Function Component を作っていて、これ経由で Context にユーザーの状態を currentUser として持たせ、変更があるとそれを書き換えるようにしています。この AuthProvider で他のコンポーネントをラップすることで、それらのコンポーネントから currentUser を簡単に参照できるようになります。
また、currentUser は undefined、User、null の 3 つの型を可としてあり、それぞれ Firebase API コールとの兼ね合いで下記のように利用しています。
- undefined:API コールの結果が返る前
- User:API コールの結果、ログインだった場合 User オブジェクトが返る
- null:API コールの結果、未ログインの場合 null が返る
こうすることで、API コールの結果が返る前のタイミングが未ログイン状態だと判定されないようにしています。
続いて、この AuthProvider で他コンポーネントをラップする部分です。
App.tsximport "./App.css"; import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import { AuthProvider } from "./Auth"; import Home from "./pages/Home"; import Login from "./pages/Login"; import Signup from "./pages/Signup"; const App: React.FC = () => { return ( <Router> <Switch> <AuthProvider> // ここでラップしている <Route exact path="/" component={Home} /> <Route exact path="/signup" component={Signup} /> <Route exact path="/login" component={Login} /> </AuthProvider> </Switch> </Router> ); }; export default App;こうすることで、AuthProvider 配下のコンポーネントであれば Context にアクセスできるようになります。
続いて会員登録画面を見てみます。
Signup.tsximport React, { Fragment, useContext, useEffect, useState } from "react"; import { Button, Container, FormControl, Grid, Link, TextField, Typography } from "@material-ui/core"; import { AuthContext } from "../Auth"; import auth from "../firebase"; const Signup = (props: any) => { // useContextにContext全体を与えて、Contextが持つcurrentUserを取得 const { currentUser } = useContext(AuthContext); const [email, setEmail] = useState<string>(""); const [password, setPassword] = useState<string>(""); useEffect(() => { // 前回はここでauth.onAuthStateChanged()を呼んでいたがその必要がない currentUser && props.history.push("/"); // [currentUser]により、currentUserが変化した際にuseEffect内を発動できる // undefined → APIコール結果を受取る → User or null になる → useEffect内発動 という流れ }, [currentUser]); return ( (略) ); }; export default Signup;前回よりも少しだけスッキリ書けています。
注意点は useEffect 第二引数で、currentUser が無いと useEffect が画面描画時しか呼ばれません。アプリのロード時にこの画面が描画されると、currentUser が undefined のままそれ以降 useEffect が呼ばれず、リダイレクトがうまくいかなくなってしまいます。ログイン画面も同様なので、説明は省略します。
最後にホーム画面も見てみます。
Home.tsximport React, { Fragment, useContext, useEffect } from "react"; import { Button, Container, Grid, Typography } from "@material-ui/core"; import { AuthContext } from "../Auth"; import auth from "../firebase"; const Home = (props: any) => { const { currentUser } = useContext(AuthContext); useEffect(() => { // currentUserが明示的にnullの場合はログイン画面へリダイレクト currentUser === null && props.history.push("/login"); }, [currentUser]); return ( (略) ); }; export default Home;こちらも前回より少しだけスッキリ書けています。
次回
アプリ全体の状態管理がより上手に行えるようになり、規模の拡大に対する耐性が増しました。
次回は Formik と Yup を使い、フォームのバリデーションができるようにしていきます。
- 投稿日:2020-01-23T16:00:57+09:00
GatsbyでNotionなソースにする
プラグイン
- gatsby-source-notionso | GatsbyJS
- いくつか候補はありましたが、公開対象の親ページを指定出来るという点でこちらにしました
- まだ活用出来ていませんが、GraphQLのプロパティに
slug
やisDraft
というのもあり、urlに指定したり下書きに使えそうです実際の様子
- https://blog.bear.tokyo/
- https://github.com/km-tr/blog
- GatsbyJS + Notion + Netlifyです
感想
What's New?を待ちたい
- 投稿日:2020-01-23T14:02:11+09:00
ReactNative(Expo)でイベントトラッキング 〜expo-analyticsの使い方をざっくりまとめる〜
ReactNativeでイベントトラッキングする時に候補に上がるのは、
- firebase
- amplitude
- expo-analytics
の3つだと思います。
今回は、そのうちの1つexpo-analyticsの紹介です。expo-analyticsはWeb版GoogleAnalyticsのイベントトラッキングをReactNativeで簡単に利用できるようにしてくれているライブラリです。
正直、WebサイトでGAのイベントトラッキングをしたことがある人は、
使い方がほぼ同じなのでこの記事を見る必要はないです笑リポジトリURL
https://github.com/ryanvanderpol/expo-analyticsEvents [イベント]
GoogleAnalyticsでの説明
https://developers.google.com/analytics/devguides/collection/analyticsjs/events?hl=jaanalytics.event(new Event('eventCategory', 'eventAction', 'eventLabel', ‘eventValue’)) 例) analytics.event(new Event('Video', 'Play', 'The Big Lebowski', 123)) .then(() => console.log("success")) .catch(e => console.log(e.message));引数の対応表
引数 フィールド名 値の型 必須 説明 第1引数 eventCategory テキスト はい 通常はインタラクションに使用されたオブジェクト(例: 'Video') 第2引数 eventAction テキスト はい インタラクションの種類(例: 'play') 第3引数 eventLabel テキスト いいえ イベントを分類する際に使用(例: 'Fall Campaign') 第4引数 eventValue 整数 いいえ イベントに関連する数値(例: 42) Custom Dimensions [カスタムディメンション]
GoogleAnalyticsでの説明
https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_ログイン状態や有料ユーザーなど独自で分析軸を定義したい時に使用する。
※最大20個までなのでトラッキングクラスでインデックスを管理する必要がある
Google analytics版の解説
https://webtan.impress.co.jp/e/2017/09/21/26869GooleAnalytics管理画面の設定手順
GA管理画面で管理→プロパティ→カスタム定義にて、カスタムディメンションを作成。
「ユーザー」「セッション」「ヒット」「商品」のどれに紐づくかを設定。実装
analytics.addCustomDimension(1(作成したディメンションのインデックス番号), 'Comedy’(ディメンション名)) 使いところわからないけど、削除も可能。 analytics.removeCustomDimension(1);Custom Metrics [カスタム指標]
GoogleAnalyticsでの説明
https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cm_数値の独自定義をしたい時に使用する、
こちらも最大20個までなのでトラッキングクラスでインデックスを管理する必要がある想定イメージ
- 電話のコール数
- アプリの起動回数
- 表示時間
フォーマット
- 整数
- 通過
- 時間
実装例
analytics.addCustomMetric(1, 15); analytics.removeCustomMetric(1);デバックモード
デバックモードをtrueにするとconsole.logに表示される。
const analytics = new Analytics('UA-XXXXXX-Y', null, { debug: true })
- 投稿日:2020-01-23T11:44:44+09:00
useGlobalState
useGlobalState.jsximport React, { useEffect, createContext, useReducer, useContext, useDebugValue } from 'react'; /* Action Types */ const SET_VALUE = 'SET_VALUE'; /* Define a context and a reducer for updating the context */ const GlobalStateContext = createContext(); const globalStateReducer = (state, action) => { const { key, data } = action.payload; switch (action.type) { case SET_VALUE: return { ...state, ...{ [key] : data }, }; default: return state; } }; export const GlobalStateProvider = ({ children }) => { const [ state, dispatch ] = useReducer(globalStateReducer, {}); return ( <GlobalStateContext.Provider value={[ state, dispatch ]}> { children } </GlobalStateContext.Provider> ); }; const useGlobalState = (key, initialValue) => { const [ state, dispatch ] = useContext(GlobalStateContext); const setGlobalState = data => { dispatch({ type: SET_VALUE, payload: { key, data }, }); }; const getState = () => { const resultState = state && state[key] ? state[key] : initialValue; return resultState; }; useEffect(() => { if ( ! state || ! state.hasOwnProperty(key)) { setGlobalState(initialValue !== undefined ? initialValue : null); } }, [ initialValue ]); return [ getState(), setGlobalState, ]; }; export default useGlobalState;index.jsximport React from 'react'; import { render } from 'react-dom'; import '@project/initialize'; import { GlobalStateProvider } from './useGlobalState'; import Contents from './Contents'; const App = () => ( <GlobalStateProvider> <Contents /> </GlobalStateProvider> ); render(<App />, document.getElementById('root'));Contents.jsximport React from 'react'; import useGlobalState from './useGlobalState'; const Contents = () => { const defaultValue = null; const [ globalValue, setGlobalValue ] = useGlobalState('key', defaultValue); return ( <div>{ globalValue }</div> ) }; export default Contents;
- 投稿日:2020-01-23T00:10:45+09:00
reducerで Array 扱うときにちょっと便利なやつ
/** * テストを満たす要素一つ (なかった場合 null) とそれを除いたリストを返す */ export const one = <T>( arr: readonly T[], matcher: (a: T) => boolean ): [T | null, readonly T[]] => { return arr.reduce(([result, others], cur) => { if (result != null) { return [result, [...others, cur]]; } return matcher(cur) ? [cur, others] : [null, [...others, cur]]; }, [null, []] as [T | null, readonly T[]]); };
- 投稿日:2020-01-23T00:10:45+09:00
reducerで配列扱うときにちょっと便利なやつ
/** * テストを満たす要素一つ (なかった場合 null) とそれを除いたリストを返す */ export const oneAndOthers = <T>( arr: readonly T[], matcher: (a: T) => boolean ): [T | null, readonly T[]] => { return arr.reduce(([result, others], cur) => { if (result != null) { return [result, [...others, cur]]; } return matcher(cur) ? [cur, others] : [null, [...others, cur]]; }, [null, []] as [T | null, readonly T[]]); }; /** * usage * reducer() { */ ... case Actions.PatchEntityById: { const { id, diff } = action.payload; const [entity, others] = oneAndOthers( state.entities, e => e.id === id, ); return patch({ entities: ...others, { ...one, ...diff } }); ...