Typescript×React×Hooksで会員管理②Contextでアプリの状態管理


前回は Typescript×React×Hooks 及び Firebase Authentication を用いて簡単な会員管理アプリを構築しました。今回はより実践的なアプリケーションとするべく、React Hooks の一部である Context(createContext、useContext)を用いて、アプリケーション全体の状態管理をできるようにしていきます。

全 3 回の内容は下記です。

  1. Firebase Auth で認証基盤外出し
  2. Context でアプリの状態管理
  3. 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.tsx
import { 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.tsx
import "./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.tsx
import 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.tsx
import 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 を使い、フォームのバリデーションができるようにしていきます。

Typescript×React×Hooksで会員管理③Formik とYupでフォームバリデーション