TLDR: Scroll down and copy the code. You only need to add your login logic.
This article will cover:
- A brief introduction to the new-ish Google Sign In api
- How to implement it using React and Typescript
- Add relevant typings on global window object
Intro
Google recently announced they are discontinuing their old auth-service "Google Sign-In" in favor for their new and improved service "Sign In With Google".
Their new service comes in two parts:
- Login button
- One Tap
You can read more about them here.
We'll cover the first one in this article, how it works and how to implement it in React with Typescript.
Compared to the old service, this one is much easier to use. It's straight-forward enough to implement the login button yourself without needing a library like (the awesome) react-google-login
that's the go-to solution for the old api.
Google Auth Introduction
I'm just going to go over the basics here.
Disclaimer: There might be a much better way to do this. I would be happy to know how, so leave a comment! I couldn't find any examples of this, so I figured I'd post my implementation and hopefully help someone else.
Although the new auth api is a bit tricky to get your head around at first when using React, we can make it work. The trick is to understand how the script loads the client and how that fits with React's loading and rendering.
The google documentation covers both the html and javascript api, and we'll be using the latter. But since we're building with React, we mostly use the step-by-step guide to figure out how the auth api works. We have to account for how React loads and renders elements. Unfortunately this means we can't just statically stick it in the header like the guide instructs.
After you followed the setup process, the documentation tells you to add a script tag to your header (in public/index.html
), but since we're using React we're not going to do that. We're going to control when and where we run that script, and thus initiate the google auth client. We're doing this because the script initiates a client and we want to pass it our own callback function that we define with react.
// The script that runs and load the new google auth client.
// We're not(!) adding it to our header like the guide says.
<script src="https://accounts.google.com/gsi/client" async defer></script>
Lets get started
First off, Typescript will complain about missing types on the window
object. We'll fix that properly later.
What we'll implement first is adding the script that loads the google auth client when our sign-in page renders, add the "target div" that the script will be looking for, and initiate the client with our callback function.
The problem
Attaching that callback-function to the google client is what makes using the new auth api with React a bit troublesome. (but even more so using the old one!). If we add the script tag to the static html like the docs say, we can't pass it any function defined in react. We could maybe handle stuff by defining a function on the server-side of things, but I want to stay within React and handle this on the front-end and use my graphql-hooks to login.
The process
When our login page renders, we'll attach the google client-script to the header from inside a useEffect
hook. We'll add an initializer-function to the onLoad
-eventlistener for that script tag. The onLoad event will then trigger and initialize the google auth client with our callback attached.
The google client will then magically find our already rendered div
with id=g_id_signin
and render the login-button.
A nice looking, personalized google sign-in button should now be visible to the user.
The code
import { Button } from "@material-ui/core"
import { useEffect, useState } from "react"
export default function GoogleSignin() {
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false)
const [user, setUser] = useState(undefined)
useEffect(() => {
if (user?._id || gsiScriptLoaded) return
const initializeGsi = () => {
// Typescript will complain about window.google
// Add types to your `react-app-env.d.ts` or //@ts-ignore it.
if (!window.google || gsiScriptLoaded) return
setGsiScriptLoaded(true)
window.google.accounts.id.initialize({
client_id: GOOGLE_CLIENT_ID,
callback: handleGoogleSignIn,
})
}
const script = document.createElement("script")
script.src = "https://accounts.google.com/gsi/client"
script.onload = initializeGsi
script.async = true
script.id = "google-client-script"
document.querySelector("body")?.appendChild(script)
return () => {
// Cleanup function that runs when component unmounts
window.google?.accounts.id.cancel()
document.getElementById("google-client-script")?.remove()
}
}, [handleGoogleSignIn, initializeGsi, user?._id])
const handleGoogleSignIn = (res: CredentialResponse) => {
if (!res.clientId || !res.credential) return
// Implement your login mutations and logic here.
// Set cookies, call your backend, etc.
setUser(val.data?.login.user)
})
}
return <Button className={"g_id_signin"} />
}
You might want to add some more implementation details here and there. But this is the gist of it! You can at least use it as a starting point. Hope it helps!
Fixing the window types
If you're using create-react-app
, you will already have the file react-app-env.d.ts
in your project root. You can add the types for the google auth api there. I translated the api documentation to typescript types. There might be some errors since I haven't used and tested all the functions. But it should be correct.
/// <reference types="react-scripts" />
interface IdConfiguration {
client_id: string
auto_select?: boolean
callback: (handleCredentialResponse: CredentialResponse) => void
login_uri?: string
native_callback?: Function
cancel_on_tap_outside?: boolean
prompt_parent_id?: string
nonce?: string
context?: string
state_cookie_domain?: string
ux_mode?: "popup" | "redirect"
allowed_parent_origin?: string | string[]
intermediate_iframe_close_callback?: Function
}
interface CredentialResponse {
credential?: string
select_by?:
| "auto"
| "user"
| "user_1tap"
| "user_2tap"
| "btn"
| "btn_confirm"
| "brn_add_session"
| "btn_confirm_add_session"
clientId?: string
}
interface GsiButtonConfiguration {
type: "standard" | "icon"
theme?: "outline" | "filled_blue" | "filled_black"
size?: "large" | "medium" | "small"
text?: "signin_with" | "signup_with" | "continue_with" | "signup_with"
shape?: "rectangular" | "pill" | "circle" | "square"
logo_alignment?: "left" | "center"
width?: string
local?: string
}
interface PromptMomentNotification {
isDisplayMoment: () => boolean
isDisplayed: () => boolean
isNotDisplayed: () => boolean
getNotDisplayedReason: () =>
| "browser_not_supported"
| "invalid_client"
| "missing_client_id"
| "opt_out_or_no_session"
| "secure_http_required"
| "suppressed_by_user"
| "unregistered_origin"
| "unknown_reason"
isSkippedMoment: () => boolean
getSkippedReason: () =>
| "auto_cancel"
| "user_cancel"
| "tap_outside"
| "issuing_failed"
isDismissedMoment: () => boolean
getDismissedReason: () =>
| "credential_returned"
| "cancel_called"
| "flow_restarted"
getMomentType: () => "display" | "skipped" | "dismissed"
}
interface Window {
google?: {
accounts: {
id: {
initialize: (input: IdConfiguration) => void
prompt: (
momentListener: (res: PromptMomentNotification) => void
) => void
renderButton: (
parent: HTMLElement,
options: GsiButtonConfiguration,
clickHandler: Function
) => void
disableAutoSelect: Function
storeCredential: Function<{
credentials: { id: string; password: string }
callback: Function
}>
cancel: () => void
onGoogleLibraryLoad: Function
revoke: Function<{
hint: string
callback: Function<{ successful: boolean; error: string }>
}>
}
}
}
}
Shameless plug
If you like this kind of stuff and are looking for a job in Sweden, Gothenburg, hit me up!
Top comments (4)
Thanks for writing this! It helped me get started with migrating our React app to the new library. I ran into a few issues with this that I ended up writing about in case it was helpful to anyone: dolthub.com/blog/2022-05-04-google...
Hi @mremanuel
There are some issues with your code snippet:
making the
clickHandler
function optional and not specifying a value for it fixes this problem for me.I lived in Goteborg...just not anymore...but I'd still work for ya.