I've been struggling with this problem for a while. I have an Auth component inside which I try to access to local storage to see if there is a token in there and send it to server to validate that token.
if token is valid the user gets logged-in automatically.
./components/Auth.tsx
const Auth: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch(); // I'm using redux-toolkit to mange the app-wide state
useEffect(() => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
if (userId) {
axios
.post("/api/get-user-data", { userId, token })
.then((res) => {
dispatch(userActions.login(res.data.user)); // the user gets logged-in
})
.catch((error) => {
localStorage.clear();
console.log(error);
});
}
}
}, [dispatch]);
return <Fragment>{children}</Fragment>;
};
export default Auth;
then I wrap every page components with Auth.tsx in _app.tsx file in order to manage the authentication state globally.
./pages/_app.tsx
<Provider store={store}>
<Auth>
<Component {...pageProps} />
</Auth>
</Provider>
I have a user-profile page in which user can see all his/her information.
in this page first of all I check if the user is authenticated to access this page or not.
if not I redirect him to login page
./pages/user-profile.tsx
useEffect(() => {
if (isAuthenticated) {
// some code
} else {
router.push("/sign-in");
}
}, [isAuthenticated]);
The problem is when the user is in user-profile page and reloads . then the user always gets redirected to login-page even if the user is authenticated.
It's because the code in user-profile useEffect gets executed before the code in Auth component.
(user-profile page is a child to Auth component)
How should i run the code in Auth component before the code in user-profile page ?
I wanna get the user redirected only when he's not authenticated and run all the authentication-related codes before any other code.
Are you sure that the problem is that user-profile's useEffect is executed before Auth's useEffect? I would assume that the outermost useEffect is fired first.
What most probably happens in your case is that the code that you run in the Auth useEffect is asynchronous. You send a request to your API with Axios, then the useEffect method continues to run without waiting for the result. Normally, this is a good situation, but in your profile, you assume that you already have the result of this call.
You would probably have to implement an async function and await the result of both the axios.post method and dispatch method. You would need something like this:
useEffect(() => {
async () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem("token")
const userId = localStorage.getItem("userId")
if (userId) {
try {
const resp = await axios.post("/api/get-user-data", {userId, token})
await dispatch(userActions.login(res.data.user)) // the user gets logged-in
} catch(error) {
localStorage.clear()
console.log(error)
}
}
}
}()
}, [dispatch])
I think this should work, but it would cause your components to wait for the response before anything is rendered.
Related
I'm working with React to build a admin panel for a website, and using Firebase as the authentication system (and data storage, etc).
I've gone through a few Private Route versions, but finally settled on the one that seems to work best with Firebase. However, there is a minor problem. It works well when the user logins in, and according to Firebase Auth documentation, by default, it should be caching the user.
However, if I log in, and then close the tab and re-open it in a new tab, I get ejected back to the login page (as it should if the user isn't logged in).
I am running the site on localhost via node, but that probably shouldn't matter. A console.log reports that the user is actually logged in, but then gets kicked back anyway. Everything is encapsulated in a useEffect, which watches the LoggedIn value, and checks the Auth State.
Is there any way to prevent this from kicking a logged-in user out when they re-open the tab?
import { FunctionComponent, useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import Routes from '../../helpers/constants';
export const PrivateRoute: FunctionComponent<any> = ({
component: Component,
...rest
}) => {
const [LoggedIn, SetLoggedIn] = useState(false);
// Check if the User is still authenticated first //
useEffect(() => {
const auth = getAuth();
onAuthStateChanged(auth, (user:any) => {
if (user.uid !== null)
{
SetLoggedIn(true);
// This gets reached after the tab is re-opened, indicating it's cached.
console.log("Logged In");
}
});
}, [LoggedIn]);
// On tab reload however, this acts as if LoggedIn is set to false after the cache check
return (
<Route
{...rest}
render={(props:any) => {
console.log(LoggedIn);
return LoggedIn ? (
<Component {...props} />
) : (
<Redirect to={Routes.LOGIN} />
);
}}
/>
);
};
It redirects because in the first render of your private route the code that sets the LoggedIn state to true hasn't been executed yet. You could use an extra boolean state to avoid rendering the Routes when you haven't checked the auth status.
...
const [LoggedIn, SetLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
...
if (user.uid !== null) {
setLoading(false);
SetLoggedIn(true);
}
...
// On tab reload however, this acts as if LoggedIn is set to false after the cache check
if(loading) return <div>Loading...</div>; // or whatever UI you use to show a loader
return (
<Route
...
/>
);
};
You'll need to check for the user only on the component mount, you can have an empty dependency array in the useEffect hook, and also stop listening for updates in the hook clean up
useEffect(() => {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, (user:any) => {
...
});
return unsubscribe; // stop listening when unmount
}, []);
But you'll be reinventing the wheel a little, there is already a hook you could use https://github.com/CSFrequency/react-firebase-hooks/tree/master/auth#useauthstate
I have this code using AppState to show a user a modal where finger print scan is required to unlock the app once the app goes to background and comes back to active
In useEffect of the HomeScreen
AppState.addEventListener('change', this.handleAppStateChange);
return () => {
AppState.removeEventListener('change', this.handleAppStateChange);
};
method
handleAppStateChange (nextAppState){
if (nextAppState === 'active') {
this.props.navigation.navigate('AuthModal');
}
This all works fine and good. On a separate screen where I have my logout, when I logout and get navigated to the login screen, the appState event is still triggered and I don't want that. The event should only be restricted to the AuthStack. I have tried a couple of things but no luck, how do I handle this?
you may use Async Storage to handle that without remove listener.
if you remove listener after logout you must listen again after login plus in app start.
solution with Async Storage without remove listener
you code may be look like this :
//in logout function
const logout = async() => {
await AsyncStorage.setItem('authState', "logout");
...
}
//in login function
const login = async() => {
await AsyncStorage.setItem('authState', "login");
...
}
and then wrap this.props.navigation.navigate('AuthModal') with if statment to check authentication
handleAppStateChange (nextAppState){
if (nextAppState === 'active') {
(async() => {
const authState = await AsyncStorage.getItem('authState');
if(authState === "login"){
this.props.navigation.navigate('AuthModal');
}
})();
}
}
I'm currently switching the authentication of my (React) application from the Auth0 react SDK to the normal SPA SDK (as I don't like the way tokens are fetched in the react SDK)
The issue I'm facing now is that when I sign in to the application I'm stuck in an endless redirection loop that is caused (I think) by the component below. Some context: the router uses protected routes that will redirect the user to the /login route that handles the authentication. Inside the component bellow auth.isAuthenticated() always returns false... That's why auth.loginWithRedirect() is called. But I can't see how to solve this...
const LoginRoute: React.FunctionComponent<any> = ({ ...props }) => {
const { isLoggedIn, loading } = useSelector(userSelector);
const dispatch = useDispatch();
useEffect(() => {
const handleAsyncLogin = async () => {
const isAuthenticated = await auth.isAuthenticated();
if (isAuthenticated) {
// Auth0 says we are logged in, so we only need to store the user in the store
dispatch(login());
} else if (!!auth && !loading) {
// We are *not* logged in. Go to Auth0 login page.
await auth?.loginWithRedirect();
}
};
handleAsyncLogin();
}, [dispatch, loading]);
return (
<Route
{...props}
component={() => {
if (isLoggedIn) {
console.log("Is Logged In");
// We are *definitely* logged in, go to projects
return <Redirect to="/questionsFromMe" />;
}
if (loading) {
return <Loader />;
}
return <div />;
}}
/>
);
};
Please help me out! Thanks in advance.
Try setting the 'Application Type' to a value that suits your app in auth0 application properties page if you have not already.
i have tried to do routing according to user check and i did but my method worked two times. i understand it from my log
i'm using redux and here is my "useeffect" in mainrouter.js
// currentUser : null // in initialState for users
useEffect(() => {
if(states.currentUser== null)
{
console.log("Checking user for routing");
actions.getCurrentUserACT();
}
else{
console.log("Logged in")
}
},[]);
my mainrouter.js return
return (
<NavigationContainer>
{
states.currentUser == null ?
(this.SignStackScreen()) :
(this.MainTabNavigator())
}
</NavigationContainer>
);
here is my "getCurrentUserAct" method in redux actions
export function getCurrentUserACT() {
return function (dispatch) {
firebase.auth().onAuthStateChanged(user => {
if (user) {
console.log("User authed..");
dispatch({type:actionTypes.CURRENT_USER,payload:user})
} else {
console.log("User notauthed..");
dispatch({type:actionTypes.CURRENT_USER,payload:null})
}
});
}
when my app start, logs are .. (if i logged in before)
Checking user for routing
User authed
User authed
i think the reason for this, useeffect worked first time and state changed which use in return and rerender"
but then i removed to condition, logs were same.
so how i can avoid this?
is there a way to run method before from useeffect and avoid second render? or wait data in useeffect?
Your code looks right and there is no reason to log two times because of useEffect because you used it as componentDidMount so it is called one time.
The reason of logging two times is this code snippet:
firebase.auth().onAuthStateChanged(user => {
here onAuthStateChanged is callback and it is called when state of authentication changed on Firebase.
If you are going to check if the user logged in then you need to avoid to do like below
if (user) { }
Please try console.log(user) and check if user keeps same on double logging.
I believe user inner properties will be changed.
Hope this helps you to understand
export function getCurrentUserACT() {
return function (dispatch) {
firebase.auth().onAuthStateChanged(user => {
dispatch(setCurrentUserACT(user))
console.log("control point" + user);
// if (user) {
// console.log("User authed..");
// dispatch({type:actionTypes.CURRENT_USER,payload:user})
// } else {
// console.log("User notauthed..");
// dispatch({type:actionTypes.CURRENT_USER,payload:null})
// }
});
}
Logs were double
Checking user for routing
control point[object Object]
control point[object Object]
______________________Edited 31.05______________________
I change my code a little bit like below
const dispatch = useDispatch();
const currentUser = useSelector(state => state.currentUserReducer);
useEffect(() => {
console.log(JSON.stringify(currentUser));
if (currentUser == null) {
console.log("Checking user for routing");
//dispatch(getCurrentUserACT());
}
else {
console.log("Logged in")
}
}, []);
I found something like hint but I can't fix because i dont know
When app start logs are
null
Checking user for routing
I didnt dispatch getCurrentUserAct(), so logs are right. If i dispatch, user will be checking (user already logged in), component rerender and here is double logs from double render.
I need dispatch before useeffect, not in useeffect
Is there a way?
I am developing an app named "GitHub Finder".
I am fetching the date in App component using async function and pass these function to User component as props and I call these functions in useEffect.
The problem is here, when I goto user page for second time it shows previous data which I passed in props from App component and then it shows loader and shows new data.
Here is App component code where I am fetching date from APIs and passing to User component through props.
// Get single GitHub user
const getUser = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}
// Get user repos
const getUserRepos = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}/repos?
per_page=5&sort=created:asc&client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setRepos(res.data);
setLoading(false);
}`
User component code.
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
}, []);
I've recorded a video, so you guys can easily understand what I am trying to say.
Video link
Check live app
How can I solve this problem?
Thank in advance!
Here is what happens in the app :
When the App component is rendered the first time, the state is user={} and loading=false
When you click on a user, the User component is rendered with props user={} and loading=false, so no spinner is shown and no data.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user1 and set loading=false (now the user data is rendered)
When you go back to search page, the app state is still user=user1 and loading=false
Now when you click on another user, the User component is rendered with props user=user1 and loading=false, so no spinner is shown and the data from previous user is rendered.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user2 and set loading=false (now the new user data is rendered)
One possible way to fix this problem :
instead of using the loading boolean for the User component, inverse it and use loaded
When the User component is unmounted clear the user data and the loaded boolean.
App component:
const [userLoaded, setUserLoaded] = useState(false);
const getUser = async username => {
await setUserLoaded(false);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
await setUser(res.data);
setUserLoaded(true);
};
const clearUser = () => {
setUserLoaded(false);
setUser({});
};
<User
{...props}
getUser={getUser}
getUserRepos={getUserRepos}
repos={repos}
user={user}
loaded={userLoaded}
clearUser={clearUser}
/>
User component:
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
return () => clearUser();
}, []);
if (!loaded) return <Spinner />;
You can find the complete code here
Please make your setUser([]) empty at the start of getUser like this:
const getUser = async (username) => {
setLoading(true);
setUser([]);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}