How to Handle Authentication in React Applications
How to Handle Authentication in React Applications
The concept for this I have straight up stolen from Kent C. Dodds' blog post on the same concept. Go here and read his post. Maybe attend a workshop he puts on, I would bet money it would be worth the cost.
The Concept
The concept is simple. We're going to check to see if the current user is authenticated. If they are not authenticated we will present only components for unauthenticated users. If the user is authenticated then we will present only components for authenticated users. With that we will not have to constantly check if the user is logged in. We can assume one way or the other.
We will do this by wrapping the root component in several providers. In this example I am using Apollo with graphql to fetch data from the backend. If you use something else you will need to swap it out. Kent C. Dodds' version does not include this portion so that one might be of more help to you than mine will be. Or maybe not, shrug that's up to you to decide.
On load we will check for a token in local storage and attempt to fetch data about the user from the backend. If there's no token, or there is no data about the user then we know the current user is not authenticated. Else... they are authenticated.
./src/context/auth-context.js
import React from "react"
import { useQuery, useMutation } from "@apollo/react-hooks"
import { gql } from "apollo-boost"
import { AUTH_TOKEN } from "../constant"
const ME_QUERY = gql`
query MeQuery {
me {
...UserDetails
}
}
`
const LOGIN_USER_MUTATION = gql`
mutation LoginMutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
...UserDetails
}
}
}
`
const SIGNUP_USER_MUTATION = gql`
mutation SignupMutation($email: String!, $password: String!, $name: String!) {
signup(email: $email, password: $password, name: $name) {
token
user {
...UserDetails
}
}
}
`
const AuthContext = React.createContext()
function AuthProvider(props) {
const { loading, data, refetch } = useQuery(ME_QUERY)
const [login] = useMutation(LOGIN_USER_MUTATION)
const [signup] = useMutation(SIGNUP_USER_MUTATION)
const signin = (email, password) => {
return login({ variables: { email, password } }).then(res => {
if (res && res.data && res.data.login && res.data.login.token) {
const { token } = res.data.login
localStorage.setItem(AUTH_TOKEN, token)
refetch()
} else {
throw Error("No token returned")
}
return res
})
}
const register = (name, email, password) => {
return signup({ variables: { name, email, password } }).then(res => {
if (res && res.data && res.data.login && res.data.login.token) {
const { token } = res.data.login
localStorage.setItem(AUTH_TOKEN, token)
refetch()
} else {
throw Error("No token returned")
}
return res
})
}
const logout = () => {
localStorage.sremoveItem(AUTH_TOKEN)
refetch()
}
if (loading) {
return <p>Loading</p>
}
return (
<AuthContext.Provider
value={{ data, signin, logout, register }}
{...props}
/>
)
}
const useAuth = () => React.useContext(AuthContext)
export { AuthProvider, useAuth }
./src/context/user-context.js
import React from 'react'
import { useAuth } from './auth-context'
const UserContext = React.createContext()
const UserProvider = props => {
const { data } = useAuth()
return <UserContext.Provider value={data ? data.me : null} {...props} />
}
const useUser = () => React.useContext(UserContext)
export { UserProvider, useUser }
./src/context/app-context.js
import React from 'react'
import { AuthProvider } from './auth-context'
import { UserProvider } from './user-context'
import client from './apollo-client'
import { ApolloProvider } from 'react-apollo'
function AppProvider({ children }) {
return (
<ApolloProvider client={client}>
<AuthProvider>
<UserProvider>{children}</UserProvider>
</AuthProvider>
</ApolloProvider>
)
}
export default AppProvider
./src/apollo-client.js
import { HttpLink, InMemoryCache, ApolloClient } from 'apollo-client-preset'
import { WebSocketLink } from 'apollo-link-ws'
import { ApolloLink, split } from 'apollo-link'
import { getMainDefinition } from 'apollo-utilities'
import { AUTH_TOKEN } from '../constant'
const httpLink = new HttpLink({ uri: 'http://localhost:4000' })
const middlewareLink = new ApolloLink((operation, forward) => {
// get the authentication token from local storage if it exists
const tokenValue = localStorage.getItem(AUTH_TOKEN)
// return the headers to the context so httpLink can read them
operation.setContext({
headers: {
Authorization: tokenValue ? `Bearer ${tokenValue}` : '',
},
})
return forward(operation)
})
// authenticated httplink
const httpLinkAuth = middlewareLink.concat(httpLink)
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000`,
options: {
reconnect: true,
connectionParams: {
Authorization: `Bearer ${localStorage.getItem(AUTH_TOKEN)}`,
},
},
})
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
httpLinkAuth,
)
// apollo client setup
const client = new ApolloClient({
link: ApolloLink.from([link]),
cache: new InMemoryCache(),
connectToDevTools: true,
})
export default client
./src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import AppProvider from './context/app-context'
import './index.css'
ReactDOM.render(
<AppProvider>
<App />
</AppProvider>,
document.getElementById('root'),
)
./src/App.js
import React from 'react'
import { useUser } from './context/user-context'
import UnauthenticatedApp from './UnauthenticatedApp'
import AuthenticatedApp from './AuthenticatedApp'
const App = () => {
const user = useUser()
console.log('<App />', { user })
return user ? <AuthenticatedApp /> : <UnauthenticatedApp />
}
export default App