React-Redux: 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.
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
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.
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"; // highlight-line
const store = createStore(reducers); // highlight-line
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.
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.
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)
import React, { Component } from "react";
import { Link } from "react-router-dom"; // highlight-line
import { connect } from "react-redux"; // highlight-line
import { signIn, signOut } from "../actions"; // highlight-line
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); // highlight-line
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.
const INITIAL_STATE = {
isSignedIn: null
};
export default (state = INITIAL_STATE, action) => {
};
then we can complete the rest of the reducer.
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
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.
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()); // highlight-line
// 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) { // highlight-line
return null;
} else if (this.props.isSignedIn) { // highlight-line
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); // highlight-line
GitHub Repo
Ncoughlin: React-Streams-Client
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.