I am using expo and AWS Cognito for my app. I am trying to divert users who are authenticated to the home screen while those who are not to the sign-in screen but cannot get the expected behavior to work. The main issue seems to be when users sign out the app does not refresh, so the AWS token is being pulled from local storage so that the user still appears to be logged in. Only when I do a hard refresh does it show they are now logged out.
Here is my App.js code to log in the user:
const App = () => {
const [userID, setUserID] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const userInfo = await Auth.currentAuthenticatedUser(
{ bypassCache: true }
);
console.log(userInfo.attributes.sub);
if (!userInfo) {
return;
}
if (userInfo) {
const userData = await API.graphql(
graphqlOperation(
getUser,
{ id: userInfo.attributes.sub,
}
)
)
if (userData.data.getUser) {
console.log(userData.data.getUser);
setUserID(userData.data.getUser)
return;
} else {
setUserID(null);
}
}
}
fetchUser();
}, [])
return (
<AppContext.Provider value={{
userID,
setUserID: ({}) => setUserID({}),
}}>
<AppNavigation/>
</AppContext.Provider>
);
}}
And here is my code for the App Navigation where I have my conditional statement:
const AppNavigation = () => {
const { userID } = useContext(AppContext);
console.log(userID) //this gives the correct value, null when not logged in and a user object when the user is logged in.
return (
<NavigationContainer>
<Drawer.Navigator
drawerContent={props => <DrawerContent { ...props} />}
drawerPosition='left'
initialRouteName={userID === null ? 'SignIn' : 'HomeDrawer'}
>
<Drawer.Screen
name='HomeDrawer'
component={MainNavStack}
/>
<Drawer.Screen
name='SignIn'
component={SignIn}
/>
</Drawer.Navigator>
</NavigationContainer>
I have tried every combination of useState and useEffect I can think of:
using getAuthenticatedUser on the AppNavigation screen and then setting a state if successful.
passing props from App.js directly
not using AppContext at all
putting it all in useEffect
getting the authenticated user directly from a function in the conditional statement
The problem seems to be that initialRouteName is determining the route before I can set any kind of state in the app. How can I get the expected behavior without having to hard refresh the app?
Related
I have react native app and i am implementing auth flow using Context API
the nature of the app is when user open it won't request to login or signup and user can explore the app screens however when user add items to cart and about to checkout then will be requested to sign-in/sign-up to continue.
let's assume these are the screens
Home-> shop -> cart
so when user in the cart will be asked to login, after login user value in context provider will be updated and they continue from same screen the user logged in (which in this case is cart screen)
However, when provider value updated all screens re-render again and redirect to initial route which home screen.
how can i handle this scenario which not re-render all screen again.
In my app i am passing the navigation stuck as a children to context provider as follow:
<AuthProvider>
<Routes />
</AuthProvider>
and here is the code of context provider:
authProvider.js:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, setUser }),
[user]);
return (
<AuthContext.Provider
value={{
value,
login: async (email, password) => {
//here i am validating the login details and update user state
// if login successful
setUser(/*some data related to user */)
//when i set user data here it re-render all screens again
// if login failed
setUser(null)
}
}}>
{children}
</AuthContext.Provider >
and the following the Routes.js which i am passing it as children to the above code:
const Routes = () => {
const { value } = useContext(AuthContext);
// validate token with backend
useEffect(() => {
setTimeout(async () => {
const Token = await AsyncStorage.getItem('Token');
if (Token === null) {
value.setUser(null)
} else {
await axios.get(URL, {
headers: { 'x-auth-token': Token },
}).then(async (res) => {
const {data} = res;
value.setUser(data)
}).catch(async (err) => {
if (err.response) {
await AsyncStorage.removeItem('Token');
value.setUser(null)
}
});
}
}, 1000)
}, []);
return (
<>
<NavigationContainer>
//this is the stuck where i have the three screen mentioned above (home, shop, cart)
<AppStuckScreen />
</NavigationContainer>
</>
)
}
export default Routes;
In cart screen i am checking the user values stored in context provider if null then open the login form.
I hope someone can help me on how to handle this situation where not to re-render all component again when updating user value in Context provider.
Why don't you keep the current screen in the Context? So after a successful login, you just navigate to that screen.
The situation is as follows. In my application, I use a router, and pass information to it whether the user is identified or not, similar to the role of the administrator. The data is stored in the auth context, put there auth hook, and used in App.js. The entire code is below. The problem is that when I reload the page, I get redirected from any tab to the home page. This happens because of a momentary change in App.js when constructing the App component, the variables in the useRoutes(isAuthenticated, admin) function change to false and true(true - after identification) when the page is reloaded. I'm relatively new to React, and don't really understand how to solve this issue. All I want to achieve is to make sure that the variables don't change their values in the App by simply refreshing the page.
App.js
function App() {
const { token, login, logout, admin } = useAuth()
const isAuthenticated = !!token
const routes = useRoutes(isAuthenticated, admin)
return (
<AuthContext.Provider value={{
token, login, logout, isAuthenticated, admin
}}>
<Router>
<div className="app-container">
{routes}
</div>
</Router>
</AuthContext.Provider>
);
}
AuthContext.js
function noop() { }
export const AuthContext = createContext({
token: null,
login: noop,
logout: noop,
isAuthenticated: false,
admin: false,
})
Auth.hook.js
const storageName = 'userData'
export const useAuth = () => {
const [token, setToken] = useState(null)
const [admin, setAdmin] = useState(false)
const login = useCallback((jwtToken, isAdmin) => {
setToken(jwtToken)
setAdmin(isAdmin)
localStorage.setItem(storageName, JSON.stringify({
token: jwtToken,
isAdmin: isAdmin,
}))
}, [])
const logout = useCallback(() => {
setToken(null)
setAdmin(false)
localStorage.removeItem(storageName)
}, [])
useEffect(() => {
const data = JSON.parse(localStorage.getItem(storageName))
if (data && data.token) {
login(data.token, data.isAdmin) // <-- There some problem
}
}, [login])
return { login, logout, token, admin }
}
I solved this problem with a simple solution :)
All you need to do is just add 1 more variable ready in the context with a value of false . Then set it in auth.hook.js to true after login. And export it to App.js and use it like if(ready){return "page"} else return <>Loading</>.
I'm using react-router-dom to secure the entire application. All routes are protected under a ProtectedRoute component (see code below), which redirects to an external url, a single-sign-on (SSO) page if the user is not logged in.
Problem:
When the user goes to '/home', they get a brief glimpse (a "flash") of the protected route before getting redirected to 'external-login-page.com/' (the login page). How do I avoid the flashing so that the user only sees the login page?
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) { // redirect if not logged in
return (
<Route
component={() => {
window.location.href = 'http://external-login-page.com/';
return null;
}}
/>
);
} else {
return <Route {...rest} />;
}
};
window.location.href can be called earlier to prevent flashing. Also in your specific case what you probably want is to render nothing at all when the user is not authenticated.
The code may look like this:
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) { // redirect if not logged in
window.location.href = 'http://external-login-page.com/';
return null;
} else {
return <Route {...rest} />;
}
};
You might consider the Redirect component
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) {
return <Redirect to='https://external-login-page.com/' />
} else {
return <Route {...rest} />;
}
};
I would guess that invoking window directly + return null is rendering the React app for a split second before the page reloads.
You can use the Redirect component in a simpler way like this.
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
children,
...rest
}) => {
return <Route {...rest} render={() => isAuthenticated ? children : <Redirect to='http://external-login-page.com/' />}
}
Posting the solution that eventually worked for me: instead of blocking by Router, block by App.
The key is to split your App into two components, AuthenticatedApp and UnauthenticatedApp. From there, lazy load the correct component depending on the user's level of access. This way, if they're not authorized, their browser won't even load AuthenticatedApp at all.
AuthenticatedApp is a component to your entire app, providers, routers, etc. Whatever you had in App.tsx originally should go here.
UnauthenticatedApp is a component that you want your users to see when they're not allowed to access the application. Something like "Not authorized. Please contact admin for help."
App.tsx
const AuthenticatedApp = React.lazy(() => import('./AuthenticatedApp'));
const UnauthenticatedApp = React.lazy(() => import('./UnauthenticatedApp'));
// Dummy function to check if user is authenticated
const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
const getUser = () => sleep(3000).then(() => ({ user: '' }));
const App: React.FC = () => {
// You should probably use a custom `AuthContext` instead of useState,
// but I kept this for simplicity.
const [user, setUser] = React.useState<{ user: string }>({ user: '' });
React.useEffect(() => {
async function checkIfUserIsLoggedInAndHasPermissions() {
let user;
try {
const response = await getUser();
user = response.user;
console.log(user);
setUser({ user });
} catch (e) {
console.log('Error fetching user.');
user = { user: '' };
throw new Error('Error authenticating user.');
}
}
checkIfUserIsLoggedInAndHasPermissions();
}, []);
return (
<React.Suspense fallback={<FullPageSpinner />}>
{user.user !== '' ? <AuthenticatedApp /> : <UnauthenticatedApp />}
</React.Suspense>
);
};
export default App;
Read Kent C Dodd's great post about it here [0]!
EDIT: Found another great example with a similar approach, a bit more complex - [1]
[0] https://kentcdodds.com/blog/authentication-in-react-applications?ck_subscriber_id=962237771
[1] https://github.com/chenkie/orbit/blob/master/orbit-app/src/App.js
I could use your input on a quick question about Component loads.
The Goal
Return the <Login /> Component if the user isn't logged in, and the App if they are.
Expected Behavior
When a user is logged in, they see the App.
Observed Behavior
The <Login /> Component flickers (renders) for a moment, then the user sees the App.
My goal is to eliminate this flicker!
Code Samples
Index.js
export default function Index() {
let [isLoading, setIsLoading] = useState(true)
const router = useRouter()
// User object comes in from an Auth Context Provider
const { user } = useContext(AuthContext)
const { email } = user
useEffect(() => {
if (user) {
setIsLoading(false)
}
}, [])
// Returns the App if logged in, login screen if not
const getLoggedIn = () => {
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
}
return (
<Box className="App">
{ isLoading
? <div className={classes.root}>
<LinearProgress />
</div>
: getLoggedIn()
}
</Box>
)
}
Auth Context
Note: I'm using Firebase for auth.
// Listens to auth state changes when App mounts
useEffect(() => {
// Calls setUser state update method on callback
const unsubscribe = onAuthStateChange(setUser)
return () => {
unsubscribe()
}
}, [])
// Brings data from auth to Auth Context user state via callback
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
Stack
"next": "^8.1.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
Thanks so much for taking a look.
I had this exact problem and resolved it by storing the user in local storage
then on app start up do this:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')))
and it'll use the details from localstorage and you wont see a flicker
(it's because onauthstate takes longer to kick in)
So I figured out a sort of 'hacky' way around this. One needs to set the value of the boolean on which the initial load of the App depends...
const getLoggedIn = () => {
// Right here
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
...before making any asynchronous calls in the AuthContext. Like this:
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
// sets loggedIn to true to prevent flickering to login screen on load
callback({ loggedIn: true })
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
I hope this helps someone.
I have the following (redux) state:
{
authentication: user
}
When logged out, user is set to null.
I have the following components:
const Dashboard = ({ authentication }) => {
if (!authentication.user) {
return <Redirect to={"/login"} />
}
return (
<SomeInnerComponent />
);
}
const SomeInnerComponent = ({ authentication }) => {
const name = authentication.user.name;
return (
<h1>Hello, {name}</h1>
)
}
authentication is mapped using connect and mapStateToProps. I would think that when I am logged out that I would be redirected, but I get an error instead: authentication.user is null.
Why does the if-statement in Dashboard not redirect me? I also tried wrapping it in a useEffect with authentication as a dependency.
In our app, we redirect unauthenticated users by history.replace history docs
or you read docs again, maybe you can find mistake in your code reacttraining
I fixed it by writing a custom hook:
export function useAuthentication() {
const history = useHistory();
const user = useSelector(state => state.authentication.user);
const dispatch = useDispatch();
useEffect(() => {
if (!user) {
history.push(LOGIN);
});
return { user };
}
Which can then be called in my React components as follows:
const Dashboard = () => {
const { user } = useAuthentication();
return (
// My code
);
}