Authentication in React Applications


May 20, 2019

Video Blogger

Photo by Mike Enerio


How to handle user authentication in modern React Applications with context and hooks

Skipping to the end

Here's the secret to this blog post in one short code example:

1import React from 'react'

2import {useUser} from './context/auth'

3import AuthenticatedApp from './authenticated-app'

4import UnauthenticatedApp from './unauthenticated-app'

5

6function App() {

7 const user = useUser()

8 return user ? <AuthenticatedApp /> : <UnauthenticatedApp />

9}

10

11export App

That's it. 99% of apps which require authentication of any kind can be drastically simplified by that one little trick. Rather than trying to do something fancy to redirect the user when they happen to land on a page that they're not supposed to, instead you don't render that stuff at all. Things get even cooler when you do this:

1import React from 'react'

2import {useUser} from './context/auth'

3

4const AuthenticatedApp = React.lazy(() => import('./authenticated-app'))

5const UnauthenticatedApp = React.lazy(() => import('./unauthenticated-app'))

6

7function App() {

8 const user = useUser()

9 return user ? <AuthenticatedApp /> : <UnauthenticatedApp />

10}

11

12export App

Sweet, now you don't even bother loading the code until it's needed. So the login screen shows up faster for unauthenticated users and the app loads faster for authenticated users.

What the <AuthenticatedApp /> and <UnauthenticatedApp /> do is totally up to you. Maybe they render unique routers. Maybe they even use some of the same components. But whatever they do, you don't have to bother wondering whether the user is logged in because you make it literally impossible to render one side of the app or the other if there is no user.

How do we get here?

If you want to just look at how it's all done, then you can checkout the bookshelf repo which I made for my Build ReactJS Applications Workshop.

Ok, so what do you do to get to this point? Let's start by looking at where we're actually rendering the app:

1import React from 'react'

2import ReactDOM from 'react-dom'

3import App from './app'

4import AppProviders from './context'

5

6ReactDOM.render(

7 <AppProviders>

8 <App />

9 </AppProviders>,

10 document.getElementById('root'),

11)

And here's that <AppProviders /> component:

1import React from 'react'

2import {AuthProvider} from './auth-context'

3import {UserProvider} from './user-context'

4

5function AppProviders({children}) {

6 return (

7 <AuthProvider>

8 <UserProvider>{children}</UserProvider>

9 </AuthProvider>

10 )

11}

12

13export default AppProviders

Ok, cool, so we have a provider from the app's authentication and one for the user's data. So presumably the <AuthProvider /> would be responsible for bootstrapping the app data (if the user's authentication token is already in localStorage then we can simply retrieve the user's data using that token). Then the <UserProvider /> would be responsible for keeping the user data up to date in memory and on the server as we make changes to the user's data (like their email address/bio/etc.).

The auth-context.js file has some stuff in it that's outside the scope of this blog post/domain specific, so I'm only going to show a slimmed down/modified version of it:

1import React from 'react'

2import {FullPageSpinner} from '../components/lib'

3

4const AuthContext = React.createContext()

5

6function AuthProvider(props) {

7

8

9

10

11

12

13

14 if (weAreStillWaitingToGetTheUserData) {

15 return <FullPageSpinner />

16 }

17

18 const login = () => {}

19 const register = () => {}

20 const logout = () => {}

21

22

23

24

25 return (

26 <AuthContext.Provider value={{data, login, logout, register}} {...props} />

27 )

28}

29

30const useAuth = () => React.useContext(AuthContext)

31

32export {AuthProvider, useAuth}

33

34

35

36

37

38

39

The key idea that drastically simplifies authentication in your app is this:

The component which has the user data prevents the rest of the app from being rendered until the user data is retrieved or it's determined that there is no logged-in user

It does this by simply returning a spinner instead of rendering the rest of the app. It's not rendering a router or anything at all really. Just a spinner until we know whether we have a user token and attempt to get that user's information. Once that's done, then we can continue with rendering the rest of the app.

Conclusion

Many apps are different. If you're doing server-side rendering then you probably don't need a spinner and you have the user's information available to you by the time you start rendering. Even in that situation, taking a branch higher up in the tree of your app drastically simplifies the maintenance of your app.

I hope this is helpful to you. You can checkout the bookshelf repo (or even edit it on codesandbox) for a more complete picture of what all this is like in a more realistic scenario with all the pieces together.