React: Integrating Google OAuth2 with Redux 👮‍♂️

Intro

In the last post we worked on setting up our Twitch Clone “Glitch” with user authentication using Google OAuth2 in a component called GoogleAuth using a class component. Currently the state of a users login status is confined to this component. Here is where we are currently.

components/GoogleAuth.js
import React, { Component } from "react"
import { Link } from "react-router-dom"

class GoogleAuth extends Component {
  state = { isSignedIn: null }

  componentDidMount() {
    window.gapi.load("auth2", () => {
      window.gapi.auth2
        .init({
          client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
          scope: "email",
        })
        .then(() => {
          // create auth variable
          this.auth = window.gapi.auth2.getAuthInstance()
          // update state so that component will re-render
          this.setState({ isSignedIn: this.auth.isSignedIn.get() })
          // listen for changes to authentication status
          this.auth.isSignedIn.listen(this.onAuthChange)
        })
    })
  }

  // updates auth state to current auth status
  // triggered when authentication status changes
  onAuthChange = () => {
    this.setState({ isSignedIn: this.auth.isSignedIn.get() })
  }

  onSignInClick = () => {
    this.auth.signIn()
  }

  onSignOutClick = () => {
    this.auth.signOut()
  }

  renderAuthButton() {
    if (this.state.isSignedIn === null) {
      return null
    } else if (this.state.isSignedIn) {
      return (
        <button onClick={this.onSignOutClick} className="ui red google button">
          <i className="google icon" />
          Sign Out
        </button>
      )
    } else {
      return (
        <button onClick={this.onSignInClick} className="ui red google button"> 
          <i className="google icon" />
          Sign In
        </button>
      )
    }
  }

  render() {
    return (
      <Link to="/" className="item">
        <div>{this.renderAuthButton()}</div>
      </Link>
    )
  }
}

export default GoogleAuth

We want user authentication status to be available to us globally so we are going to introduce redux to this workflow so that the login status is globally available in the store.

Note that this isn’t strictly necessary, you could design your application in such a way that the GoogleAuth component is always present. We are going to do this anyways.

We are essentially going to be sending this isSignedIn piece of state around in a big loop directly back to this component so that it can render the Google buttons correctly. We will need to send it to the Store and then receive it back in the component in the form of a prop.

Two Potential Architectures

There are two potential architectures to this integration. We could follow the typical Redux format where the API calls would be made by the action creators in the actions index, or we could communicate with the API directly from the GoogleAuth component. Either way, the state is going to be looped through the store and back to the GoogleAuth component.

Typical Redux Architecture

typical redux architecture

Contained Authorization Architecture

contained authorization architecture

The big difference here is in which part of the application is making the queries to the Google API. The queries are the same, it’s simply a question of who is sending them out.

In this instance we are going to use the contained architecture.

Scaffolding Redux

Let us start by scaffolding out redux. Let’s start with the index.js file.

index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";import { createStore } from "redux";
import App from "./components/App";
import reducers from "./reducers";
const store = createStore(reducers);
ReactDOM.render(
  // wrap app in provider
  <Provider store={store}>    <App />  </Provider>,  document.querySelector("#root")
);

Then we also create a reducers and actions directory and place an index file inside each of those. And we can scaffold a quick blank actions file.

actions/index.js
import { combineReducers } from 'redux';

export default combineReducers( {
    replaceMe: () => "replace me"
});

And this just has a placeholder function so that the app doesn’t crash.

Create Actions

Then we can create some very simple actions. These do not even require a payload.

actions/index.js
export const signIn = () => {
  return {
    type: "SIGN_IN",
  };
};

export const signOut = () => {
  return {
    type: "SIGN_OUT",
  };
};

When we create our reducer we can have it return a Boolean flag based solely on the action type.

Wiring Actions to Component

The part here that may confuse you is that we are passing isSignedIn into onAuthChange as an argument. This becomes very clear if you just understand that isSignedIn is returning a boolean. So we are basically saying that this

if (isSignedIn === true)

is the same as this

if (true === true)

which is the same as just

if(true)

components/GoogleAuth.js
import React, { Component } from "react";
import { Link } from "react-router-dom";import { connect } from "react-redux";
import { signIn, signOut } from "../actions";
class GoogleAuth extends Component {
  // this will soon be replaced
  state = { isSignedIn: null };

  componentDidMount() {
    window.gapi.load("auth2", () => {
      window.gapi.auth2
        .init({
          client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
          scope: "email",
        })
        .then(() => {
          // create auth variable
          this.auth = window.gapi.auth2.getAuthInstance();
          // update state so that component will re-render
          this.setState({ isSignedIn: this.auth.isSignedIn.get() });
          // listen for changes to authentication status
          this.auth.isSignedIn.listen(this.onAuthChange);
        });
    });
  }

  // triggered when authentication status changes
  // isSignedIn returns boolean
  onAuthChange = (isSignedIn) => {    if (isSignedIn) {      this.props.signIn();    } else {      this.props.signOut();    }  };
  // manually trigger GAPI auth change
  onSignInClick = () => {
    this.auth.signIn();
  };

  onSignOutClick = () => {
    this.auth.signOut();
  };

  // helper function
  renderAuthButton() {
    if (this.state.isSignedIn === null) {
      return null;
    } else if (this.state.isSignedIn) {
      return (
        <button onClick={this.onSignOutClick} className="ui red google button">
          <i className="google icon" />
          Sign Out
        </button>
      );
    } else {
      return (
        <button onClick={this.onSignInClick} className="ui red google button">
          <i className="google icon" />
          Sign In
        </button>
      );
    }
  }

  render() {
    return (
      <Link to="/" className="item">
        <div>{this.renderAuthButton()}</div>
      </Link>
    );
  }
}

// we will make mapStateToProps soon, passing in null for now
export default connect(null, { signIn, signOut })(GoogleAuth);

Reducer

When our redux application first boots up our reducer gets called one time to initialize it. We previously in our component had set a default auth state value of null, but because we are modifying this so that the auth state value is coming from the store, we need to set an initial value in the reducer, and we set it to null.

We can do this by creating an object called initial state and passing it in. The convention of naming the object in all caps is a signal to other engineers to never modify this constant.

reducers/authReducer.js
const INITIAL_STATE = {
    isSignedIn: null
};

export default (state = INITIAL_STATE, action) => {

};

then we can complete the rest of the reducer.

reducers/authReducer.js
const INITIAL_STATE = {
    isSignedIn: null
};

export default (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case 'SIGN_IN':
            return {...state, isSignedIn: true };
        case 'SIGN_OUT':
            return {...state, isSignedIn: false };
        default:
            return state;
    }
};

and then we can update our reducers index

reducers/index.js
import { combineReducers } from 'redux';
import authReducer from './authReducer';

export default combineReducers( {
    auth: authReducer
});

Now the last thing that we need to do is close the loop by communicating the state in our store back to the GoogleAuth component.

Replace State with Props

We need to go back to our component now and replace any reference to state with a reference props, since that is where our state is now contained.

components/GoogleAuth.js
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { connect } from "react-redux";

import { signIn, signOut } from "../actions";

class GoogleAuth extends Component {
  // reference to state has been removed

  componentDidMount() {
    window.gapi.load("auth2", () => {
      window.gapi.auth2
        .init({
          client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
          scope: "email",
        })
        .then(() => {
          // create auth variable
          this.auth = window.gapi.auth2.getAuthInstance();
          // can now use logic of onAuthChange for initial render
          this.onAuthChange(this.auth.isSignedIn.get());          // listen for changes to authentication status
          this.auth.isSignedIn.listen(this.onAuthChange);
        });
    });
  }

  // triggered when authentication status changes
  onAuthChange = (isSignedIn) => {
    if (isSignedIn) {
      this.props.signIn();
    } else {
      this.props.signOut();
    }
  };

  // manually trigger GAPI auth change
  onSignInClick = () => {
    this.auth.signIn();
  };

  onSignOutClick = () => {
    this.auth.signOut();
  };

  // helper function
  renderAuthButton() {
    if (this.props.isSignedIn === null) {      return null;
    } else if (this.props.isSignedIn) {      return (
        <button onClick={this.onSignOutClick} className="ui red google button">
          <i className="google icon" />
          Sign Out
        </button>
      );
    } else {
      return (
        <button onClick={this.onSignInClick} className="ui red google button">
          <i className="google icon" />
          Sign In
        </button>
      );
    }
  }

  render() {
    return (
      <Link to="/" className="item">
        <div>{this.renderAuthButton()}</div>
      </Link>
    );
  }
}

const mapStateToProps = (state) => {  return { isSignedIn: state.auth.isSignedIn };}
export default connect(mapStateToProps, { signIn, signOut })(GoogleAuth);

GitHub Repo

Ncoughlin: React-Streams-Client