I'm trying to create protected routes that are only viable while user is logged in, but I have trouble getting loggedIn state in ProtectedRoutes component, it's always set to false thus redirecting to "/login". What am I not getting correctly here?
App.tsx
interface loginContextInterface {
loggedIn: boolean;
setLoggedIn: (value: (((prevState: boolean) => boolean) | boolean)) => void;
}
export const LoginContext = createContext({} as loginContextInterface);
export default function App() {
const [ loggedIn, setLoggedIn ] = useState(false)
useEffect(() => {
console.log("before", loggedIn)
isLoggedIn().then((r) => {
console.log("R", r)
setLoggedIn(r)
})
console.log("after", loggedIn)
}, [loggedIn])
return (
<LoginContext.Provider value={{loggedIn, setLoggedIn}}>
<Router>
<MenuHeader />
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/tasks" element={<ProtectedRoutes/>}>
<Route path="/" element={<Tasks/>}/>
</Route>
<Route path="/login" element={<Login />}/>
<Route path="/logout" element={<Logout />}/>
<Route path="/register" element={<Register/>}/>
</Routes>
</Router>
</LoginContext.Provider>
);
}
ProtectedRoutes.tsx
export const ProtectedRoutes = () =>{
const location = useLocation();
const {loggedIn} = useContext(LoginContext)
console.log("protected", loggedIn)
return (
loggedIn ? <Outlet/> : <Navigate to={"/login"} replace state={{location}}/>
);
}
Edit:
isLoggedIn just authenticates that the user is logged in via cookie using api on the server side. Added logging
Produces these after trying to access /tasks route and redirecting me to /login again
VM109:236 protected false
App.tsx:21 before false
App.tsx:26 after false
App.tsx:21 before false
App.tsx:26 after false
2App.tsx:23 R true
App.tsx:21 before true
App.tsx:26 after true
App.tsx:23 R true
There is an issue with the useEffect hook, using the loggedIn state value as the dependency. You should not use dependencies that are unconditionally updated by the hook callback. My guess here is that you wanted to do an initial authentication check when the app mounts. You can remove loggedIn from the dependency since it's not referenced at all.
useEffect(() => {
isLoggedIn().then(setLoggedIn);
}, []);
I suggest also using an initial loggedIn state value that doesn't match either the authenticated or unauthenticated states, i.e. something other than true|false. This is so the ProtectedRoutes can conditionally render null or some loading indicator while any pending authentication checks are in-flight and there isn't any confirmed authentication state already saved in state.
Update the context to declare loggedIn optional.
interface loginContextInterface {
loggedIn?: boolean;
setLoggedIn: React.Dispatch<boolean>;
}
Update App to have an initially undefined loggedIn state value.
const [loggedIn, setLoggedIn] = useState<boolean | undefined>();
Update ProtectedRoutes to check for the undefined loggedIn state to render a loading indicator and not immediately bounce the user to the login route.
const ProtectedRoutes = () => {
const location = useLocation();
const { loggedIn } = useContext(LoginContext);
if (loggedIn === undefined) {
return <>Checking authentication...</>; // example
}
return loggedIn ? (
<Outlet />
) : (
<Navigate to={"/login"} replace state={{ location }} />
);
};
Also in App, remove/move the "/tasks" path from the layout route rendering the ProtectedRoutes component to the nested route rendering the Tasks component. The reason is that it's invalid to nest the absolute path "/" in "/tasks".
<Route element={<ProtectedRoutes />}>
<Route path="/tasks" element={<Tasks />} />
</Route>
It's not recommended to reset the dependency inside the useEffect(), it may cause an infinite loop.
useEffect(() => {
// loggedIn will be update here and trigger the useEffect agan
isLoggedIn().then((r) => setLoggedIn(r))
}, [loggedIn])
What does the console.log(loggedIn) and console.log(r) show? I'm guessing isLoggedIn returns false, loggedIn is set to false initially so useEffect not being triggered again and it remains as false
Related
I have component AppRouter which is placed inside <BrowserRouter> and returns public or private <Route> components depending on whether the user is authenticated. Private routes are returning inside <PageWrapper> component. It contains header and sidebar, so routes are rendering in main part of this wrapper.
I am having those exceptions: React has detected a change in the order of Hooks called by AppRouter. & Rendered more hooks than previous render
Console
This is AppRouter:
export const AppRouter = () => {
const user = useAppSelector(state => state.authReducer.user)
const dispath = useAppDispatch();
useEffect(() => {
const userData = getUser()
if (userData !== null) {
dispath(authSlice.actions.updateUser(userData))
}
}, [])
const routes: IRoute[] = user === undefined ? publicRoutes : privateRoutes
let indexElement: IRoute | undefined
const routeComponents = (
<Routes>
{routes.map<React.ReactNode>((route: IRoute) => {
if (route.index === true) {
indexElement = route
}
return <Route
path={route.path}
element={route.component()}
key={route.path}
/>
})}
{
indexElement !== undefined && <Route path='*' element={<Navigate to={indexElement.path} replace />} />
}
</Routes>
)
if (user === undefined) {
return routeComponents
}
return (
<PageWrapper>
{routeComponents}
</PageWrapper>
)
}
This exception is thrown when user is authenticated and react renders component from private route(private route is only one now). It started to throw when I added useEffect(arrow function with console.log and empty dependecies array) to this private component. If i remove useEffect from this component - exceptions will not be thrown. I tried to change routes.map to privateRoutes.map - then exceptions does not throw, but I can't understand the reason why it works so.
Project is react + typescript + redux toolkit
I'm writing an app that pulls my GitHub repos and displays certain information from them (descriptions, etc.). The app is set up so that if I am not 'initialized' (meaning I haven't sent up an account), then I am forced to the 'CreateAccount' page. The state of being 'initialized' is held in local storage. But, if anyone else wants to look at the app, they won't be 'initialized', so I need a second check to see if there is anything in the database (records for each repo). If so, then the viewer should be 'initialized' and sent to the Home page. All of this is done in the App component:
const App = () => {
const setupCtx = useContext(SetupContext);
const devCtx = useContext(DevDataContext);
// variable to control routing
let initialized = null;
if (localStorage.getItem("jtsy-signin") === "true") {
initialized = true;
} else {
API.findRepo()
.then((repo) => {
console.log('APP found 1 repo', repo)
if (repo) {
initialized = true
localStorage.setItem('jtsy-signin', 'true')
console.log('1 REPO initialized', initialized)
} else {
initialized = false;
}
})
}
useEffect(() => {
if (initialized) {
console.log('APP useEffect signin=true, redirect to Home page');
if (localStorage.getItem('jtsy-login') === 'true') {
setupCtx.updateLoggedIn();
}
// console.log('APP devUpdated', setupCtx.state.devUpdated)
if (setupCtx.state.devUpdated) {
API.getActiveDevData().then((activeDevData) => {
// console.log('APP activeDevData', activeDevData.data);
const developerData = {
developerLoginName: activeDevData.data.developerLoginName,
developerGithubID: activeDevData.data.developerGithubID,
repositories: activeDevData.data.repositories,
fname: activeDevData.data.fname,
lname: activeDevData.data.lname,
email: activeDevData.data.email,
linkedInLink: activeDevData.data.linkedInLink,
resumeLink: activeDevData.data.resumeLink,
active: true
}
console.log('APP after DB call', developerData)
devCtx.updateDev(developerData)
setupCtx.updateInitialized();
setupCtx.updateDevUpdated(false)
})
}
};
}, [setupCtx.state.devUpdated])
return (
<div className='App'>
<React.Fragment>
<Router>
<Switch>
{console.log('IN APP SWITCH initialized', initialized)}
{initialized ? (
<Route exact path="/" component={Home} />
) : (
<Route exact path="/" component={CreateAccountComp} />
)}
<Route exact path="/contact" component={Contact} />
<Route exact path="/about" component={About} />
<Route exact path="/developer" component={Developer} />
<Route exact path="/login" component={LoginModal} />
<Route exact path="/logout" component={LogoutModal} />
<Route exact path="/signin" component={CreateAccountComp} />
<Route exact path="/settings" component={Settings} />
<Route component={NoMatch} />
</Switch>
</Router>
</React.Fragment>
</div >
);
};
export default App;
The variable 'initialized' controls whether the Home or CreateAccount pages are rendered. As the code shows, first local storage is checked, and if there is a value there, 'initialized' is set to 'true' and the Home page renders. This works fine. But, if there is no value in local storage, I next call findRepo(), which executes a .findOne() in the Mongo database. Then, the value of 'initialized' is set accordingly. The delay in getting a response from the database is too long. If there is no value in local storage, initialized is 'null' when rendering occurs, so the app always goes to the "CreateAccount" page. I can tell that the database call is responding properly and initialized is getting set to 'true', but it's too late (CreateAccount is already rendered).
I tried putting the database call inside of useEffect, and in a separate useEffect, but neither worked. I need a better approach to make this work.
So I have went through your code a little and came up with a solution to add a loading flag to state and use that to render a loading screen if we are waiting on any sort of update.
You could make this loading screen anything you wanted to show when waiting for updates.
Below is the code changes made all in App.js
// add isLoading flag to state
const [state, setState] = useState(
{
loggedIn: null,
sync: false,
initialized: null,
isLoading: true
}
)
Anytime setState is called, update the isLoading flag to true.
// line 55 in App.js
setState({
...state,
initialized: true,
isLoading: false
})
// line 75
setState({
...state,
initialized: true,
isLoading: false
})
// line 103
setState({
...state,
initialized: false,
isLoading: false
})
Then the return update.
{state.isLoading && <div>Loading...</div>}
<Route exact path="/" component={state.initialized ? Home : CreateAccountComp} />
With these changes the loading screen will show when we are waiting on any sort of update to initialized. Then the home screen will once it is updated to true or the create account page will show if it is updated to false.
I'm currently working on a project with Firebase Integration and React.
I can register and login, but I do want some Guard in order to have access to the "connected" pages when I'm connected, or to be redirect to the default page for disconnected state if I'm not.
I coded the following for having the different Routes and set it in an App.tsx file :
const ConnectedRoute: React.FC<RouteProps> = ({ component, ...rest }) => {
var route = <Route />
var connected = isConnected()
console.log(connected)
if (connected){
route = <Route {...rest} component={component}/>
}
else{
route = <Route {...rest} render={() => <Redirect to ="/welcome" />} />
}
return route
}
const UnconnectedRoute: React.FC<RouteProps> = ({ component, ...rest }) => {
var route = <Route />
var connected = isConnected()
console.log(connected)
if (connected){
route = <Route {...rest} render={() => <Redirect to ="/home" />} />
}
else{
route = <Route {...rest} component={component}/>
}
return route
}
const App: React.FC = () => {
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/" render={() => <Redirect to="/welcome" />} />
<UnconnectedRoute path="/welcome" component={UnconnectedHome} exact />
<ConnectedRoute path="/home" component={ConnectedHome} exact />
<UnconnectedRoute path="/login" component={Login} exact />
<UnconnectedRoute path="/register" component={Register} exact />
<ConnectedRoute path="/events/create" component={CreateEvent} exact />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
And for the firebase initialization I did the following :
export async function initializeApp(){
console.log("Checking if app is initialized...")
if (firebase.apps.length == 0){
console.log("App initializing...")
firebase.initializeApp(firebaseConfig);
await firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
}
}
And the function to know if a user is connected (as a cookie) is this one :
export function isConnected(){
const res = firebase.auth().currentUser
console.log(res)
return res !== null
}
Still, when I reload the tab, it's always returning me FALSE !
So, I was wondering how could I init the firebase server before the launch of the App ? Is this the current problem ? I currently do not have any clue about it and it frustrates me so much...
If you have already encountered such a problem, that would really help me !
Thank you !
Firebase Authentication persists the user's authentication state in local storage. But when you load a new page (or reload the existing one), it has to check with the server whether the authentication state is still valid. And this check takes some time, during which there is no current user yet.
The typical way to get around such race conditions is to use an auth state listener to detect the current user:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in, redirect to "connected" pages
} else {
// No user is signed in, probably require them to sign in
}
});
I tried to answer in comment with the code, it didn't work.
I did this for now (it's not that good but at least it works) :
const connectedPaths =
[
"/home",
"/events/create"
]
const unconnectedPaths =
[
"/welcome",
"/login",
"register"
]
firebase.initializeApp(firebaseConfig);
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
if (!connectedPaths.includes(window.location.pathname)) window.location.href = "/home"
}
else {
if (!unconnectedPaths.includes(window.location.pathname)) window.location.href = "/welcome"
}
});
I want to send information to second page if the user is logged in . I would like use Context to that.
Something about my code :
const Login = () => {
...
const [logged, setLogged] = React.useState(0);
...
const log = () => {
if (tempLogin.login === "Login" && tempLogin.password == "Haslo") {
setLogged(1);
}
...
return (
{logged == 1 && (
<Redirect to="/page" />
)}
I want to send logged to /page but i don't know how . None guide help me .Page is actually empty React file.
There are 2 ways handle that:
Passing state to route(as described in docs):
{logged == 1 && (
<Redirect to={{ path: "/page", state: { isLoggedIn: true } }} />
)}
And your component under /page route will access that flag as this.props.location.state.isLoggedIn
Utilize some global app state(Redux, Context API with <Provider> at root level or anything alike).
To me second option is better for keeping auth information:
Probably not only one target component will want to check if user is authorized
I'd expect you will need to store some authorization data to send with new requests(like JWT token) so just boolean flah accessible in single component would not be enough.
some operation on auth information like say logout() or refreshToken() will be probably needed in different components not in single one.
But finally it's up to you.
Thanks skyboyer
I solved this problem with Context method .I will try tell you how i do this becouse maybe someone will have the same problem
I created new file
import React from "react";
import { Redirect } from "react-router";
const LoginInfo = React.createContext();
export const LoginInfoProvider = props => {
const [infoLog, setInfoLog] = React.useState("");
const login = name => {
setInfoLog(name);
};
const logout = () => {
setInfoLog("old");
};
const { children } = props;
return (
<LoginInfo.Provider
value={{
login: login,
logout: logout,
infolog: infoLog
}}
>
{children}
</LoginInfo.Provider>
);
};
export const LoginInfoConsumer = LoginInfo.Consumer;
In App.js add LoginInfoProvider
<Router>
<LoginInfoProvider>
<Route exact path="/" component={Login} />
<Route path="/register" component={Register} />
<Route path="/page" component={Page} />
</LoginInfoProvider>
</Router>
In page with login (parts of code in my question) i added LoginInfoConsumer
<LoginInfoConsumer>
So i am building a React+redux app using firebase. I'm using react-router onEnter callback function(checkAuth) for route protection.
export default function getRoutes (checkAuth) {
return (
<Router history={browserHistory}>
<Route path='/' component={MainContainer}>
<IndexRoute component = {HomeContainer} onEnter = {checkAuth}/>
<Route path='auth' component = {AuthenticateContainer} onEnter = {checkAuth} />
<Route path='feed' component = {FeedContainer} onEnter = {checkAuth} />
<Route path='logout' component = {LogoutContainer} />
</Route>
</Router>
)
}
the checkAuth function calls checkIfAuthed function to see if there is a currentUser.
function checkAuth (nextState, replace) {
const isAuthed = checkIfAuthed(store)
console.log('isAuthed from checkAuth method', isAuthed)
const nextPathName = nextState.location.pathname
console.log('nextPathName', nextPathName)
// debugger
if (nextPathName === '/' || nextPathName === 'auth') {
if (isAuthed === true) {
// debugger
replace('feed')
}
} else {
// debugger
if (isAuthed !== true) {
// debugger
replace('auth')
}
}
}
ReactDOM.render(
<Provider store = {store}>
{getRoutes(checkAuth)}
</Provider>,
document.getElementById('app')
)
The checkIfAuthed function looks like this:
export function checkIfAuthed (store) {
// debugger
// const user = firebase.auth().currentUser
firebase.auth().onAuthStateChanged((user) => {
console.log('isAuthed from on state changed', user)
// debugger
if (user === null) {
// debugger
return false
} else if (store.getState().isAuthed === false) {
// debugger
const userInfo = formatUserInfo(user.displayName, user.photoURL, user.uid)
// debugger
store.dispatch(authUser(user.uid))
// debugger
store.dispatch(fetchingUserSuccess(user.uid, userInfo))
// debugger
return true
}
// debugger
return true
})
}
However, const isAuthed is always undefined at runtime in the checkAuth() function.thus leading to replace("feed") never running. I was expecting it to be false or true.
Additionally if I instead use const user = firebase.auth().currentUser in the checkIfAuthed function the Replace function runs but this requires user to hit login button, whereas the firebase observer above automatically runs.
You can check here this implementation
The Service when we read the user auth and set the value to Redux https://github.com/x-team/unleash/blob/develop/app/services/authService.js
The reducer when set the user state to the redux state object https://github.com/x-team/unleash/blob/develop/app/reducers/userReducer.js
The action creators https://github.com/x-team/unleash/blob/develop/app/actions/UserActions.js
The login state check
https://github.com/x-team/unleash/blob/develop/app/components/UnleashApp.jsx#L17
The most important part is the authService, let me know any question