0

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?

3
  • 1
    useEffect is called after PrivateRoute component has mounted and by the time its called which in turn calls login function, user has already been redirected to the /login route. Commented Mar 10, 2021 at 14:53
  • 2
    Inside the PrivateRoute component, check the value of the authenticated only after login function has been called. Created a demo that shows you how you can solve the problem. (try changing the value of token variable inside the useEffect hook) Commented Mar 10, 2021 at 15:05
  • This demo above is great. Seems best to just keep up with a loading status for auth, which allows components to render, but you can use the loading status to essentially wait for auth to happen. Commented Dec 21, 2021 at 0:50

2 Answers 2

1

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}/>
}
Sign up to request clarification or add additional context in comments.

Comments

0

Thanks to Yousaf for the explaination in the comments and the HospitalRun project on GitHub, I made a loading state in the <App /> component.

import { useAuth } from './context/AuthContext'
import { useEffect, useState } from 'react'

import Router from './Router'

const App = () => {
    const [ loading, setLoading ] = useState(true)
    const { login } = useAuth()

    const token = localStorage.getItem('user')

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

        setLoading(false)
    }, [loading, token, login])

    if (loading) return null

    return <Router />
}

export default App

Here I only let anything render, after the login function was called.

if (loading) return null

If this could be done any better, feedback would still be appriciated!

1 Comment

I would say that loading is a redundant state since it's the opposite boolean value from authenticated, right? I imagine you could consume authenticated instead. It seems your useEffect is for initial state purposes, since every subsequent execution comes from login|out being called somewhere else. If you try to initialize your state with localStorage value(which is a common approach) as I suggested I believe you could only check authenticated directly if I'm not mistaken.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.