DEV Community

Cover image for Let's Make a Redux + Material UI + OAuth Starter template! Pt 2. Login/Logout
Ethan Soo Hon
Ethan Soo Hon

Posted on

Let's Make a Redux + Material UI + OAuth Starter template! Pt 2. Login/Logout

In this part of the tutorial we will take the client ID we got in part 1 and hook it up to a Login and Logout component that will use the react-google-login library. Here's the packages you need

"dependencies": {
    "@material-ui/core": "^4.11.2",
    "@material-ui/icons": "^4.11.2",
    "@material-ui/lab": "^4.0.0-alpha.57",
    "@material-ui/styles": "^4.11.2",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-google-login": "^5.2.2",
    "react-redux": "^7.2.2",
    "react-scripts": "4.0.1",
    "redux": "^4.0.5"
  }
Enter fullscreen mode Exit fullscreen mode

(Already assuming you have your CRA + redux setup)

The documentation for react-google-login is very helpful and concise. The only thing I feel is missing are instructions to get your Google OAuth client id which I covered in the 1st article.
Now we could use the built in components shipped with the library and supply them with specific props to look something like this...

  <GoogleLogin
    clientId="zzzzz"
    buttonText="Login"
    onSuccess={responseGoogle} //Some function 
    onFailure={responseGoogle}
  />
  //same for GoogleLogout component
Enter fullscreen mode Exit fullscreen mode

This is serviceable but we eventually want to set up a theme for our project and have the styles of all our components switch when we go from light mode -> dark mode. So we will create our own Login and Logout button from Material UI components and use the hooks useGoogleLogin() useGoogleLogout() also in this library to make our buttons.

useGoogleLogin

The props that went into the GoogleLogin component are now passed into this hook function as an object

The useGoogleLogout() hook is pretty much identical in functionality so the most important part is now writing the onSuccess and onFailure functions for both. Let's check the docs to see what those functions should do

onSuccess callback

onFailure callback

The expected object to be returned if the authentication succeeds is something with the tokenId, profileObj, googleId (etc...) properties. If that same request fails an object is sent to onFailure that includes an error property along with some details about the message.

Time to Code

...With that we have everything we need to start coding! Since we will store user information in the redux store let's set up those pieces first

Setting up your redux folders is highly opinionated but this is how I set up my folders

|- reducers
|- types
|- actions
Enter fullscreen mode Exit fullscreen mode

1) Types:
Fairly simple there are only 2 possible states; the user logging in and the user logging out

export const SIGN_IN = 'SIGN_IN';
export const SIGN_OUT = 'SIGN_OUT';
Enter fullscreen mode Exit fullscreen mode

2) Action Creators:
We'll write two functions that return action objects one for Login and one for Logout.

/*
  Will be passed in as the onSuccess and onFailure callback
  for useGoogleLogin() hook
/*
export const googleOAuthLogin = (response) => {
    const action = { type: SIGN_IN };
    let payload;
    if (typeof response === 'undefined' || response.error) {
        //If login fails
        payload = null;
    } else {
        payload = response;
    }
    action.payload = payload;
    return action;
};
Enter fullscreen mode Exit fullscreen mode

For the logout we don't necessarily need to handle any errors (see we didn't even accept an argument here) because all we're really doing is just clearing the user out of the redux store.

/*
  Will be passed in as the onSuccess and onFailure callback
  for useGoogleLogout() hook
/*
export const googleOAuthLogout = () => {
    const action = { type: SIGN_OUT };
    return action;
};
Enter fullscreen mode Exit fullscreen mode

3) Reducer
I chose to store this piece of state in the redux store under the "auth" key which has an initial state I defined to be...

const INITIAL_STATE = {
    loggedIn: false,
    user: null
};
Enter fullscreen mode Exit fullscreen mode

We write the reducer handling both SIGN_IN and SIGN_OUT actions (don't forget to use default parameters!). For SIGN_IN our action.payload is null if it failed, but if it succeeded our Google response object should be in there.

case SIGN_IN:
 let loggedIn = action.payload ? true : false;
 return loggedIn ?
 {...state,loggedIn,user: {                      
    tokenId: action.payload.tokenId,                     
    ...action.payload.profileObj
   }
 }
: { ...state, loggedIn, user: null };

/*
 If successful looks state looks like 
 {loggedIn: true, user: {tokenId: "sometoken" , profileObj: {}}
*/

case SIGN_OUT:
 return { ...INITIAL_STATE};
Enter fullscreen mode Exit fullscreen mode

4) Login Component

Without talking too much about the actual CSS used to make the buttons let's quickly examine how to use the built-in styling system for Material UI

import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
    center: {
        display: 'flex',
        justifyContent: 'center'
    },
    button: {
        textTransform: 'none',
        marginTop: theme.spacing(10),
        display: 'flex',
        alignItems: 'center',
        boxShadow: theme.shadows[3],
        backgroundColor: theme.palette.primary.main,
        color: theme.palette.primary.contrastText,
        transition: 'background-color 0.5s',
        '&:hover': {
            backgroundColor: theme.palette.primary.dark,
            transition: 'background-color 0.5s',
            cursor: 'pointer'
        }
    },
}));
Enter fullscreen mode Exit fullscreen mode

We return an object from the makeStyles callback that has key value pairs where the keys are class names you wish to use and the value is always an object that specifies CSS properties. (Keep in mind CSS properties are camelCased)

*What is the theme variable? *

Since we don't have a theme defined yet Material UI supplies a default theme for us that you can see here (its just a giant object)

To use these classes we simply call the function above (we called it useStyles) and we get back our object.

const classes = useStyles();
return(
<p className={classes.center}> Hello! </p>
)
Enter fullscreen mode Exit fullscreen mode

I will be using the hooks provided by react-redux but this is equivalent to use mapStateToProps (replaced by useSelector) and mapDispatchToProps(replaced by useDipatch; we have to manually dispatch it)

...
import googleLogo from '../images/google-logo.png';
import { useDispatch } from 'react-redux';
...


const clientId =
    'blahblahblahblah.apps.googleusercontent.com';
function Login() {
    const onSuccess = (res) => {
        dispatch(googleOAuthLogin(res));
    };
    const onFailure = (res) => {
        dispatch(googleOAuthLogin(res));
    };

    const { signIn } = useGoogleLogin({
        onSuccess,
        onFailure,
        clientId,
        isSignedIn: true
    });

    return (
        <Container component='section' className={classes.center}>
            <Button className={classes.button} onClick={signIn}>
                <Avatar src={googleLogo} className={classes.avatar} />
                <Typography component='p' variant='h6' className={classes.text}>
                    Sign in with Google
                </Typography>
            </Button>
        </Container>
    );
}
Enter fullscreen mode Exit fullscreen mode
We use clientId from article 1, and isSignedIn is a boolean used to persist the users login across sessions via a entry in localStorage


To keep the styling consistent we use the same makeStyles object for the Logout component the only difference is the functions passed into useGoogleLogout are called onFailure and onLogoutSuccess.


function Logout() {
    const classes = useStyles();
    const dispatch = useDispatch();
    const onSuccess = (res) => {
        dispatch(googleOAuthLogout(res));
    };
    const onFailure = (res) => {
        dispatch(googleOAuthLogout(res));
    };
    const { signOut } = useGoogleLogout({
        onFailure,
        onLogoutSuccess: onSuccess,
        clientId,
        isSignedIn: true
    });
    return (
        <Container component='section' className={classes.center}>
            <Button className={classes.button} onClick={signOut}>
                <Avatar src={googleLogo} className={classes.avatar} />
                <Typography component='p' variant='h6' className={classes.text}>
                    Sign out of Google
                </Typography>
            </Button>
        </Container>
    );
}

Enter fullscreen mode Exit fullscreen mode

Last Step!

Lets go to App.js (or whatever your root component is called) and add conditional rendering for these components. We'll reach into the store with useSelector and check the auth.loggedIn property to determine what button to show.

function App() {
    const auth = useSelector((state) => state.auth);
    return (
            <main>
                {auth.loggedIn ? <Logout /> : <Login />}
            </main>
        </ThemeProvider>
    );
}
Enter fullscreen mode Exit fullscreen mode

Your project should look something like this now!

Part 3 will set up the Alert system through redux and the theme system so we can easily switch from light to dark mode.

Top comments (0)