Search code examples
reactjsreact-router-dom

Navigate on button click using protected routes in react


I have App.js which looks like this

<Router>
     <Routes>
         <Route element={<AnonymousUserRoute />}>
              <Route path='/user/login' element={<UserLoginScreen />}/>
         </Route>
         <Route element={<ProtectedUserRoute />} >
              <Route path='/' element={<HomeScreen />} exact />
         </Route>
     </Routes>
</Router>

ProtectedUserRoute.js

function ProtectedUserRoute() {
    const loginPageLink = "/user/login";
    const token = //getting from local storage

    return (
        token ? <Outlet /> : <Navigate to={loginPageLink} replace />
    );
};

AnonymousUserRoute.js

function AnonymousUserRoute() {
    const homePageLink = "/";
    const token = //getting from local storage

    return (
        token ? <Navigate to={homePageLink} replace /> : <Outlet />
    );
};

Login.js

function UserLoginScreen() {
    ...
    const handleLoginClick = (event) => {
        event.preventDefault();
        dispatch(loginUser(email, password));
        navigate('/');
    }
    ...
}

The problem I am facing is that, once handleLoginClick is clicked, the navigate function is not redirecting me to home screen immediately because ProtectedUserRoute doesn't allow i think. But upon refreshing the page, my route identifies that there is a token in local storage and redirects to home screen. How can i ensure, navigation happens without refresh here ?

EDIT

reducers.js

export const userLoginReducer = (state = { userDetails: { isLoggedIn: false } }, action) => {
    switch(action.type) {
        case USER_LOGIN_REQUEST:
            return { loading: true }
        case USER_LOGIN_SUCCESS:
            return { loading: false, isLoggedIn: action.payload }
        case USER_LOGIN_FAIL:
            return { loading: false, error: action.payload }
        default:
            return state
    }
}

actions.js

export const loginUser = (email, password) => async (dispatch) => {
    const loginUrl = //loginUrl
    try {
        dispatch({ type: USER_LOGIN_REQUEST })
        
        const loginData = { email: email, password: password }
        const config = { headers: { 'Content-type': 'application/json'} }
        const { data } = await axios.post(loginUrl, loginData, config)
        
        dispatch({ type: USER_LOGIN_SUCCESS, payload: true })
        
        storeUserLoggedInDetailsInLocalStorage(data.tokens); //calling a function which stores in localstorage. Nothing else done here.
    } catch(error) {
        dispatch({
            type: USER_LOGIN_FAIL,
            payload: error.response && error.response.data.detail
                    ? error.response.data.detail
                    : error.message
        })
        console.log(error)
    }
}

I have this isLoggedIn boolean value in Login screen which will be set to true once user is logged in. Below is the snippet

const userDetails = useSelector(state => state.userDetails)
const { isLoggedIn } = userDetails

EDIT 2: store.js

const userDetailsFromStorage = localStorage.getItem('userDetails') ? 
                         JSON.parse(localStorage.getItem('userDetails')) : {}

const reducer = combineReducers({
    userDetails: userLoginReducer,
})

const persistedState = {
    userDetails: {
        isLoggedIn: userDetailsFromStorage.isLoggedIn ?? false
    }
}

const middleWare = [thunk]

const store = createStore(reducer, persistedState, composeWithDevTools(applyMiddleware(...middleWare)));

export default store

Solution

  • loginUser is an asynchronous action and you should be able to await it to resolve/reject.

    Something similar to the following example:

    function UserLoginScreen() {
      ...
    
      const handleLoginClick = async (event) => {
        event.preventDefault();
    
        try {
          await dispatch(loginUser(email, password));
          navigate('/');
        } catch(error) {
          // handle or ignore errors/rejections
        }
      }
    
      ...
    }
    

    Demo

    Edit navigate-on-button-click-using-protected-routes-in-react

    The loginUser action may need to re-throw any errors it catches so UI that is waiting can catch and handle it in the UI, for example if you wanted to set some local error state or trigger a toast message to the user.