AWS Amplify offers an Authentication API that allows you to manage and store users.
Here is an example for a custom React Hook with useReducer
and useEffect
that fetches the current user data from AWS Amplify:
import { useReducer, useState, useEffect } from 'react'
import { Auth, Hub } from 'aws-amplify'
const amplifyAuthReducer = (state, action) => {
switch (action.type) {
case 'FETCH_USER_DATA_INIT':
return {
...state,
isLoading: true,
isError: false,
}
case 'FETCH_USER_DATA_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
user: action.payload.user,
}
case 'FETCH_USER_DATA_FAILURE':
return { ...state, isLoading: false, isError: true }
case 'RESET_USER_DATA':
return { ...state, user: null }
default:
throw new Error()
}
}
const useAmplifyAuth = () => {
const initialState = {
isLoading: true,
isError: false,
user: null,
}
const [state, dispatch] = useReducer(amplifyAuthReducer, initialState)
const [triggerFetch, setTriggerFetch] = useState(false)
useEffect(() => {
let isMounted = true
const fetchUserData = async () => {
if (isMounted) {
dispatch({ type: 'FETCH_USER_DATA_INIT' })
}
try {
if (isMounted) {
const data = await Auth.currentAuthenticatedUser()
if (data) {
dispatch({
type: 'FETCH_USER_DATA_SUCCESS',
payload: { user: data },
})
}
}
} catch (error) {
if (isMounted) {
dispatch({ type: 'FETCH_USER_DATA_FAILURE' })
}
}
}
const HubListener = () => {
Hub.listen('auth', data => {
const { payload } = data
onAuthEvent(payload)
})
}
const onAuthEvent = payload => {
switch (payload.event) {
case 'signIn':
if (isMounted) {
setTriggerFetch(true)
console.log('signed in')
}
break
default:
return
}
}
HubListener()
fetchUserData()
return () => {
Hub.remove('auth')
isMounted = false
}
}, [triggerFetch])
const handleSignout = async () => {
try {
console.log('signed out')
await Auth.signOut()
setTriggerFetch(false)
dispatch({ type: 'RESET_USER_DATA' })
} catch (error) {
console.error('Error signing out user ', error)
}
}
return { state, handleSignout }
}
export default useAmplifyAuth
Let’s do this step by step.
We import the Hooks API from react, as well as two modules from the ‘aws-amplify package’.
What is Hub
?
Amplify has a local eventing system called Hub. It is a lightweight implementation of Publisher-Subscriber pattern, and is used to share data between modules and components in your app. Amplify uses Hub for different tags to communicate with one another when specific events occur, such as authentication events like a user sign-in or notification of a file download.
We need to set up a listener that checks for authentication events.
// code excerpt
const HubListener = () => {
Hub.listen('auth', data => {
const { payload } = data
onAuthEvent(payload)
})
}
const onAuthEvent = payload => {
switch (payload.event) {
case 'signIn':
if (isMounted) {
setTriggerFetch(true)
console.log('signed in')
}
break
default:
return
}
}
This initializes the event listener. When a user signs in, it fires off the fetchTrigger
. fetchTrigger
is a useState
Hook that guarantees the re-render of the app as soon as we have the user data.
Otherwise, the user logs in, but the app won’t show the correct screen.
We create a reducer function called amplifyAuthReducer
that sets up all of our actions and state changes.
The magic lies within the useAmplifyAuth
function.
It takes an initial state and useReducer
.
Then we employ useEffect
with all its glory.
We track the state of the component (is it mounted?) with a simple variable to avoid race conditions. As long is isMounted
is true, we manipulate state.
At the end, we’ll clean up useEffect
and set isMounted
to false. (At the same time, we also clean up the Hub listener)
// code excerpt
useEffect(() => {
let isMounted = true
// .... more code
return () => {
Hub.remove('auth')
isMounted = false
}
}, [triggerFetch])
triggerFetch
is a dependency for the useEffect
Hook. Every time we change triggerFetch
, we run the useEffect
Hook again.
Within useEffect
we initialize two functions: fetchUserData
and HubListner
.
fetchUserData
is more or less a copy from Robin Wieruch’s article.
// code excerpt
useEffect(() => {
let isMounted = true
const fetchUserData = async () => {
// initialize the function
if (isMounted) {
dispatch({ type: 'FETCH_USER_DATA_INIT' })
}
try {
if (isMounted) {
const data = await Auth.currentAuthenticatedUser()
if (data) {
dispatch({
type: 'FETCH_USER_DATA_SUCCESS',
payload: { user: data },
})
}
}
} catch (error) {
if (isMounted) {
dispatch({ type: 'FETCH_USER_DATA_FAILURE' })
}
}
}
//...
fetchUserData() // here we call the function
//...
}, [triggerFetch])
We also provide a handleSignOut
function that we can export to our main app:
const handleSignout = async () => {
try {
console.log('signed out')
await Auth.signOut() // we use the AWS Amplify Auth module
setTriggerFetch(false)
dispatch({ type: 'RESET_USER_DATA' }) // make sure to set user to null again
} catch (error) {
console.error('Error signing out user ', error)
}
}
At the end, we expose the state and the handleSignout
function:
return { state, handleSignout }
Here is an example main app file that uses the useAmplifyAuth
Hook:
import React from 'react'
import { Authenticator, AmplifyTheme } from 'aws-amplify-react'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import HomePage from './pages/HomePage'
import ProfilePage from './pages/ProfilePage'
import MarketPage from './pages/MarketPage'
import Navbar from './components/Navbar'
import useAmplifyAuth from './components/helpers/useAmplifyAuth'
import './App.css'
export const UserContext = React.createContext()
const App = () => {
const {
state: { user },
handleSignout,
} = useAmplifyAuth()
return !user ? (
<Authenticator theme={theme} />
) : (
<UserContext.Provider value={{ user }}>
<Router>
<>
{/* Navigation*/}
<Navbar user={user} handleSignout={handleSignout} />
{/* Routes */}
<div className="app-container">
<Route exact path="/" component={HomePage} />
<Route path="/profile" component={ProfilePage} />
<Route
path="/markets/:marketId"
component={({ match }) => (
<MarketPage user={user} marketId={match.params.marketId} />
)}
/>
</div>
</>
</Router>
</UserContext.Provider>
)
}
const theme = {
...AmplifyTheme,
navBar: {
...AmplifyTheme.navBar,
backgroundColor: '#ffc0cb',
},
button: {
...AmplifyTheme.button,
backgroundColor: 'var(--amazonOrange)',
},
sectionBody: {
...AmplifyTheme.sectionBody,
padding: '5px',
},
sectionHeader: {
...AmplifyTheme.sectionHeader,
backgroundColor: 'var(--squidInk)',
},
}
export default App
Further Reading
- AWS Amplify Authentication
- How to fetch data with React Hooks? by Robin Wieruch
- A Complete Guide to useEffect by Dan Abramov
- Example Repository: amplifyagora