ReactRouterを使ってGoogleログイン前と後で画面を変える

前回に引き続きGoogle Sign-In をReactなSPAで使ってみた時の日記、2日目です。

今回はReactRouterを使って、ログイン前と後で画面を変えてみます。

ReactRouterを導入

ReactRouterを使う。バージョンは 4.2.2

$ npm install --save-prod react-router-dom

+ react-router-dom@4.2.2

とりあえず、以下のようにルーティングする。

  • / にアクセスするとトップページ
  • /login にアクセスするとログインページ

前回のコードを元に書き換えたのが以下。これを元に書いていく。

// src/App.js

import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';

class App extends Component {
  render() {
    return (
      <Router>
        <Switch>
          <Route exact path="/" component={Top}/>
          <Route path="/login" component={Login}/>
        </Switch>
      </Router>
    )
  }
}
export default App;


class Login extends Component {
  componentDidMount() {
    this.downloadGoogleScript(this.initSignInButton)
  }
  onSignIn = (googleUser) => {
    console.log('ID: ' + googleUser.getBasicProfile().getId());
  }
  downloadGoogleScript = (callback) => {
    const element = document.getElementsByTagName('script')[0];
    const js = document.createElement('script');
    js.id = 'google-platform';
    js.src = '//apis.google.com/js/platform.js';
    js.async = true;
    js.defer = true;
    element.parentNode.insertBefore(js, element);
    js.onload = () => callback(window.gapi);
  }
  initSignInButton = (gapi) => {
    gapi.load('auth2', () => {
      gapi.auth2.init({client_id: <CLIENT_ID>})
        .then(
          (result) => {
            gapi.signin2.render('google-signin-button', {
              'onsuccess': this.onSignIn,
              'onfailure': (err) => console.error(err)
            });
          },
          (err) => console.error(err)
        );
    })
  }
  render() {
    return (
      <div>
        <h1>ようこそ!</h1>
        <div id="google-signin-button"></div>
        <Link to='/'>Topへ</Link>
      </div>
    )
  }
}

class Top extends Component {
  render() {
    return (
      <div>
        <h1>Topページ</h1>
        <Link to='/login'>サインイン</Link>
      </div>
    )
  }
}

ルーティングの設計

/login へのアクセスをログイン前か後かで挙動を変える。それからログイン後でないとアクセスできないページとして /private を用意することにする。

  • /
    • ログイン前状態: ログイン前トップページを表示
    • ログイン後状態: ログイン後トップページを表示
  • /login
    • ログイン前状態: ログインページを表示 => ログインに成功すると / へ遷移
    • ログイン後状態: / へ遷移
  • /private
    • ログイン前状態: ログインページへ遷移 => ログインに成功するとアクセスしたパス( /private )へ遷移
    • ログイン後状態: プライベート画面を表示

トップページ

まずは、 / をやってみる。

  • /
    • ログイン前: ログイン前トップページを表示
    • ログイン後: ログイン後トップページを表示

ログイン済かどうかを保持するフラグを state に持たせる。ログイン後のcallbackである onSignInLogin コンポーネントから App へ移動して state を書き換えるようにする。

--- a/src/App.js
+++ b/src/App.js
@@ -2,13 +2,16 @@ import React, { Component } from 'react';
 import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
 
 class App extends Component {
+  state = {
+    authenticated: false
+  }
+  onSignIn = (googleUser) => {
+    console.log('ID: ' + googleUser.getBasicProfile().getId());
+    this.setState({authenticated: true})
+  }
   render() {
     return (
       <Router>
         <Switch>
           <Route exact path="/" component={Top}/>
-          <Route path="/login" component={Login}/>
+          <Route path="/login" render={props =>
+            <Login onSignIn={this.onSignIn} />
+          }/>
         </Switch>
       </Router>
@@ -22,9 +25,6 @@ class Login extends Component {
   componentDidMount() {
     this.downloadGoogleScript(this.initSignInButton)
   }
-  onSignIn = (googleUser) => {
-    console.log('ID: ' + googleUser.getBasicProfile().getId());
-  }
   downloadGoogleScript = (callback) => {
     const element = document.getElementsByTagName('script')[0];
     const js = document.createElement('script');
@@ -41,7 +41,7 @@ class Login extends Component {
         .then(
           (result) => {
             gapi.signin2.render('google-signin-button', {
-              'onsuccess': this.onSignIn,
+              'onsuccess': this.props.onSignIn,
               'onfailure': (err) => console.error(err)
             });
           },

認証済みかどうかを Top コンポーネントに渡して見出しの文字列を変える。

--- a/src/App.js
+++ b/src/App.js
@@ -13,7 +13,9 @@ class App extends Component {
     return (
       <Router>
         <Switch>
-          <Route exact path="/" component={Top}/>
+          <Route exact path="/" render={props => 
+            <Top authenticated={this.state.authenticated} />
+          }/>
           <Route path="/login" render={props =>
             <Login onSignIn={this.onSignIn} />
           }/>
@@ -66,9 +68,10 @@ class Login extends Component {
 
 class Top extends Component {
   render() {
+    const header = this.props.authenticated ? 'ログイン済み:Topページ' : 'ログイン前:Topページ';
     return (
       <div>
-        <h1>Topページ</h1>
+        <h1>{header}</h1>
         <Link to='/login'>サインイン</Link>
       </div>
     )

これで以下の流れができた。

  1. localhost:3000/ にアクセスすると「ログイン前:Topページ」と表示
  2. 「サインイン」リンクからログイン画面( /login )へ遷移
  3. Googleサインインでログイン
  4. 「Topへ」リンクからトップページ( / )へ遷移
  5. 「ログイン済み:Topページ」と表示

再アクセス時の挙動

ここで2つ程考えないといけないことが。

1つ目は、上のステップ5の後にブラウザをリロードしたりアドレスバーから直接アクセスした場合にログイン前状態になる、という点。これはログイン状態を state に保存しているだけだから。この挙動をどうするか。

2つ目は、リロードでログイン前状態になってしまった後、上のステップ3をやらなくてもログイン状態となる、という点。これはCookieが残るのでログイン画面( /login )に遷移しGoogleサインインボタンをレンダリングするだけで、以下の onsuccess コールバックが実行される。この挙動をどうするか。

gapi.signin2.render('google-signin-button', {
  'onsuccess': this.props.onSignIn,
  'onfailure': (err) => console.error(err)
});

セッション管理のCookieがあるから、Googleサインインがログイン成功のコールバックを実行するのは良い。だけど、ログインボタンをレンダリングする前にアプリケーション側で把握したい。それができれば上の2つとも対応できる。

アプリケーション側でログイン中かどうかを判定するには、ログイン状態を state だけでなくsessionStorageに保存すれば良いかなぁと最初は思った。有効期限にログイン時成功時に取得できる gapi.auth2.AuthResponse の有効期限を使って。

でも、Cookieの有効期限を使えば良いんじゃないかなぁと思いつつ、Googleのドキュメントを眺めてると、GoogleAuth.isSignedIn.get()というのを発見。これを使おう。

Googleサインインのgapi.auth2の初期化を Login コンポーネントでやってるけど、 App コンポーネントに移す。 Login コンポーネントではSignInボタンをレンダリングするときに gapi オブジェクトが必要なんだよな・・・。 props で渡すか。。

--- a/src/App.js
+++ b/src/App.js
@@ -3,7 +3,36 @@ import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
 
 class App extends Component {
   state = {
-    authenticated: false
+    authenticated: false,
+    gapi: null
+  }
+  componentDidMount() {
+    this.downloadGoogleScript(this.initSignInButton)
+  }
+  downloadGoogleScript = (callback) => {
+    const element = document.getElementsByTagName('script')[0];
+    const js = document.createElement('script');
+    js.id = 'google-platform';
+    js.src = '//apis.google.com/js/platform.js';
+    js.async = true;
+    js.defer = true;
+    element.parentNode.insertBefore(js, element);
+    js.onload = () => callback(window.gapi);
+  }
+  initSignInButton = (gapi) => {
+    gapi.load('auth2', () => {
+      gapi.auth2.init({client_id: <CLIENT_ID>})
+        .then(
+          (result) => {
+            if (result.isSignedIn.get()) {
+              this.setState({authenticated: true, gapi})
+            } else {
+              this.setState({authenticated: false, gapi})
+            }
+          },
+          (err) => console.error(err)
+        );
+    })
   }
   onSignIn = (googleUser) => {
     console.log('ID: ' + googleUser.getBasicProfile().getId());
@@ -17,7 +46,7 @@ class App extends Component {
             <Top authenticated={this.state.authenticated} />
           }/>
           <Route path="/login" render={props =>
-            <Login onSignIn={this.onSignIn} />
+            <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/>
           }/>
         </Switch>
       </Router>
@@ -29,31 +58,12 @@ export default App;
 
 class Login extends Component {
   componentDidMount() {
-    this.downloadGoogleScript(this.initSignInButton)
-  }
-  downloadGoogleScript = (callback) => {
-    const element = document.getElementsByTagName('script')[0];
-    const js = document.createElement('script');
-    js.id = 'google-platform';
-    js.src = '//apis.google.com/js/platform.js';
-    js.async = true;
-    js.defer = true;
-    element.parentNode.insertBefore(js, element);
-    js.onload = () => callback(window.gapi);
-  }
-  initSignInButton = (gapi) => {
-    gapi.load('auth2', () => {
-      gapi.auth2.init({client_id: <CLIENT_ID>})
-        .then(
-          (result) => {
-            gapi.signin2.render('google-signin-button', {
-              'onsuccess': this.props.onSignIn,
-              'onfailure': (err) => console.error(err)
-            });
-          },
-          (err) => console.error(err)
-        );
-    })
+    if (this.props.gapi) {
+      this.props.gapi.signin2.render('google-signin-button', {
+        'onsuccess': this.props.onSignIn,
+        'onfailure': (err) => console.error(err)
+      });
+    }
   }
   render() {
     return (

これで、ログイン後再アクセスした場合でも、「ログイン前:Topページ」ではなく「ログイン済み:Topページ」と表示されるようになった!

ローディング中表示

だけど、 gapi.auth2.init の処理が終わるまでの間「ログイン前:Topページ」と表示されてしまう。ローディング中の表示をしよう。

--- a/src/App.js
+++ b/src/App.js
@@ -39,6 +39,9 @@ class App extends Component {
     this.setState({authenticated: true})
   }
   render() {
+    if (!this.state.gapi) {
+      return ('Loading...')
+    }
     return (
       <Router>
         <Switch>

ログアウト

続いてログアウトボタンを追加。

--- a/src/App.js
+++ b/src/App.js
@@ -38,6 +38,11 @@ class App extends Component {
     console.log('ID: ' + googleUser.getBasicProfile().getId());
     this.setState({authenticated: true})
   }
+  onSignOut = () => {
+    const auth2 = this.state.gapi.auth2.getAuthInstance();
+    auth2.signOut().then(() => console.log('sign out'));
+    this.setState({authenticated: false})
+  }
   render() {
     if (!this.state.gapi) {
       return ('Loading...')
@@ -46,7 +51,7 @@ class App extends Component {
       <Router>
         <Switch>
           <Route exact path="/" render={props => 
-            <Top authenticated={this.state.authenticated} />
+            <Top authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
           }/>
           <Route path="/login" render={props =>
             <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/>
@@ -81,12 +86,20 @@ class Login extends Component {
 
 class Top extends Component {
   render() {
-    const header = this.props.authenticated ? 'ログイン済み:Topページ' : 'Topページ';
-    return (
-      <div>
-        <h1>{header}</h1>
-        <Link to='/login'>サインイン</Link>
-      </div>
-    )
+    if (this.props.authenticated) {
+      return (
+        <div>
+          <h1>ログイン済み:Topページ</h1>
+          <button onClick={this.props.onSignOut}>ログアウト</button>
+        </div>
+      )
+    } else {
+      return (
+        <div>
+          <h1>ログイン前:Topページ</h1>
+          <Link to='/login'>サインイン</Link>
+        </div>
+      )
+    }
   }
 }

おしまい

今日はここまで。次回は、ログインページ( /login ) とログイン後じゃないとアクセスできないページ( /private )をやっていく。

ここまでの結果。