Mastodon hachyterm.io

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