Skip to content
Advertisement

Unexpected output using react-router-dom with React’s Context API

For a small project of mine, I’m trying to implement the most basic authentication as possible, using the React context API without Redux.

import { createContext, useContext, useState } from 'react'

export const AuthContext = createContext()

export const useAuth = () => {
    const context = useContext(AuthContext)

    if(context === null) throw new Error('Nope')

    return context  
}

export const AuthProvider = (props) => {
    const [authenticated, setAuthenticated] = useState(false)

    const login = () => {
        setAuthenticated(true)

        localStorage.setItem(storageKey, true)
    }
    
    const logout = () => {
        setAuthenticated(false)

        localStorage.setItem(storageKey, false)
    }
    
    return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}

export default AuthContext

I created a context, and wrapped my <App /> component in it like so; <AuthProvider></App></AuthProvider>. Because I want to keep the authenticated state, I used the browser’s local storage, for storing a simple boolean value.

import PrivateRoute from './PrivateRoute'
import { useAuth } from './context/AuthContext'

import { AuthPage } from './pages'

import {
    BrowserRouter,
    Switch,
    Route,
} from 'react-router-dom'

import { useEffect } from 'react'

const App = () => {
    const { login, authenticated } = useAuth()

    useEffect(() => {
        const token = localStorage.getItem('user')

        if(token && token !== false) { login() }
    })

    return (
        <BrowserRouter>
            <Switch>
                <PrivateRoute exact path="/auth" component={AuthPage} />
                <Route exact path='/'>
                    Dashboard
                </Route>
            </Switch>
        </BrowserRouter>
    )
}

export default App

Then, in my <App /> component, I tried invoking the login callback, given from the AuthProvider, which made me assume that made me login during page refreshes. When I try to access the authenticated variable in the current component, it does work. It shows that I am authenticated.

However when I try to set up a PrivateRoute, which only authenticated users can go to like this:

import {
    Route,
    Redirect
} from 'react-router-dom'

import { useAuth } from './context/AuthContext'

const PrivateRoute = ({ component: Component, ...rest }) => {
    const { authenticated } = useAuth()
    
    if(authenticated) {
        return <Route {...rest} render={(props) => <Component {...props} />} />
    }

    return <Redirect to={{ pathname: '/login' }} />
}

export default PrivateRoute

It does not work. It just redirects me to the login page. How does this come? The PrivateRoute component is getting rendered from the <App /> component. Also, what would be the solution to this problem?

Advertisement

Answer

Rather than running a useEffect on every rerender to check if user should be logged in, you should better initialize your authenticated state with the values from your localStorage:

const storageKey = 'user'
const initialState = JSON.parse(localStorage.getItem(storageKey)) ?? false

export const AuthProvider = (props) => {
  const [authenticated, setAuthenticated] = useState(initialState)

  const login = () => {
      setAuthenticated(true)

      localStorage.setItem(storageKey, true)
  }
  
  const logout = () => {
      setAuthenticated(false)

      localStorage.setItem(storageKey, false)
  }
  
  return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}
Advertisement