Is your feature request related to a problem? Please describe.
A user navigates to my webapp. I have a header on the webapp that depends on the user's login state (eg. a button says "Account" if the user is logged in or "Sign Up" if the user is not logged in). I don't want to use the Authenticator component because I don't want to force the user to login immediately.
Describe the solution you'd like
In my header's render function, something like:
// header.tsx
import Auth from '@aws-amplify/auth';
const header = () => (Auth.isUserLoggedIn()) ? <div>Account</div> : <div>Sign In</div>;
Describe alternatives you've considered
I believe that this code is close, but I think that it forces the user to login in order to view the header, which not necessary
const header = ({authState}) => (
(authState === `signedIn`) ? <div>Account</div> : <div>Sign In</div>;
)
return withAuthenticator(header);
Additional context
Add any other context or screenshots about the feature request here.
@ajhool judging from this and your other recent issue I think you're at the same point I'm at, going through the tutorials and slowly realizing the intended workflow doesn't apply to you. All the methods I could find to do this are async, so there was no obvious way to do this just from a render function. I ended up using the Authenticator component manually (i.e. don't use withAuthenticator unless you're forcing the user to log in), and I have my top-level component provide the user to children as React context.
let [user, setUser] = useState(null)
useEffect(() => {
let updateUser = async authState => {
try {
let user = await Auth.currentAuthenticatedUser()
setUser(user)
} catch {
setUser(null)
}
}
Hub.listen('auth', updateUser) // listen for login/signup events
updateUser() // check manually the first time because we won't get a Hub event
return () => Hub.remove('auth', updateUser) // cleanup
}, []);
This code is in my top level component's render function (using React hooks), and then it uses React context to provide the user to its children. This seems to work, although there is probably a small window where you render without having the user, so there's a flash when it updates. I'm just learning Amplify so I don't know if this is the best way to do it.
Thanks, again, @all-iver ! I'm also using Authenticator manually, but with the added complication that Authenticator breaks SSR at import time, so I have a pretty heavy workaround just to get Authenticator to defer import to client side and still connect to the rest of Amplify; I try to use it sparingly and might eject completely
I assume you've looked at the source of Authenticator, but it looks like your strategy eliminates all of the fluff to achieve the desired isSignedIn flag. Nice job, it looks great! I'm not familiar with React hooks but I think useEffect is only called by the client, so your approach might be SSR non-breaking out of the box
https://github.com/aws-amplify/amplify-js/blob/master/packages/aws-amplify-react/src/Auth/Authenticator.jsx
Ah, yeah I ran into that problem with breaking imports too. :) For me just importing Auth from @aws-amplify/auth (and Hub from @aws-amplify/core) fixed my build, and I have to configure it with Auth.configure() instead of Amplify.configure(). I think those are the only changes I had to do to get it working iirc... I'm only using Authenticator though, nothing else in Amplify yet.
Oh yeah, I also had to import Authenticator like this:
import { Authenticator } from 'aws-amplify-react/dist/Auth'
import { AmplifyTheme } from 'aws-amplify-react/dist/AmplifyTheme'
The Authenticator issue became more problematic when I started using API and Storage. I found that the Authenticator component (when dynamically imported) was not properly connected to API and Storage, also the aws-amplify-react package wasn't properly hiding components. At the time, I was fairly sure that I isolated the behavior but it's possible that the problem was actually caused by having multiple versions of amplify installed in the various dependencies.
// This import breaks code
// import { Authenticator, Greetings, Loading } from 'aws-amplify-react';
let _Authenticator;
let _Greetings;
let _Loading;
let isServer = true;
componentDidMount() {
...
// Dynamically load the components client-side.
const { Authenticator, Greetings, Loading } = await import('aws-amplify-react');
let _Authenticator = Authenticator;
let _Greetings = Greetings;
let _Loading = Loading;
isServer = false;
...
}
render(){
{
!isServer && (<_Authenticator hide={[_Greetings, _Loading]} >
.....
<_Authenticator/>)
isServer && <div />
}
This is what I'm currently using and it is working, so I haven't tried moving to something more simple. But at the time, this was quite a pain to figure out. -- if your solution works, just stick with it. Definitely expect a lot of bumps in the road as you add more components
Per your second post, that's smart. Nice find
Tested out @all-iver 's answer and it's great. Also amazed by React hooks -- hadn't gotten around to learning them, but they're really useful and backwards compatible. Just to extend @all-iver 's example, here is what your code should look like, all together:
Two remaining issues:
updateUser()
is not await updateUser()
, there will be a ~1.5 ( #2154) second flash of webpage where the user is presumed not to be logged in. This could be resolved by using a localStorage isLoggedIn
flag that can make a much faster guess. That flag management could also live inside useUserStatus
. Maybe there's an Amplify field that could serve as a quick guess, too. I'm finding that this delay is a major issue in the experience of the websiteAnother solution would be to set the initial state of user
to be undefined or something like that -- then the three states are: undefined (still waiting for an answer from the server -> display splash screen), null (server says nobody is logged in), CognitoUser (sever says somebody is logged in).
// useuserstatus.ts
'use strict'
import React, { useState, useEffect } from 'react';
import Auth from '@aws-amplify/Auth';
import { Hub } from '@aws-amplify/core';
/**
* userUserStatus is a react hook that tracks the user's login status and provides a "isLoggedIn" flag that can be checked in code.
*/
function useUserStatus() {
let [user, setUser] = useState(null)
useEffect(() => {
let updateUser = async () => {
try {
let user = await Auth.currentAuthenticatedUser()
setUser(user)
} catch {
setUser(null)
}
}
Hub.listen('auth', updateUser) // listen for login/signup events
// we are not using async to wait for updateUser, so there will be a flash of page where the user is assumed not to be logged in. If we use a flag
updateUser() // check manually the first time because we won't get a Hub event
return () => Hub.remove('auth', updateUser) // cleanup
}, []);
return user;
}
export default useUserStatus;
// header.tsx
import useUserStatus from './useuserstatus';
const Header = () => {
const userStatus = useUserStatus();
const isLoggedIn = (null !== userStatus);
return (isLoggedIn) ? <div>Hello {user.attributes.email} <button>Sign Out</button></div> :
(<div>
<a href='/signin'>
<button>Login</button>
</a>
</div>);
}
I also named the component "useUserStatus", but it's really more of a "useCurrentUser" component... if you just want a simple "isLoggedIn" flag, then you should:
user
and setUser
variables to isLoggedIn
and setIsLoggedIn
setUser(user)
with `setIsLoggedIn( (null === user) );This component will automatically update when the user logs in or logs out.
I haven't tested this code, but this would be my initial guess about how [I have tested this code, and this is my strategy] for using localStorage
to do this. If you're using SSR, you might need to check a request header instead of checking the localStorage:
// useisloggedin.
'use strict'
import React, { useState, useEffect } from 'react';
import Auth from '@aws-amplify/Auth';
import { Hub } from '@aws-amplify/core';
/**
* userUserStatus is a react hook that tracks the user's login status and provides a "isLoggedIn" flag that can be checked in code.
*/
const _guessInitialLoginStatus = (): boolean => {
if( isServer() ){
// Not sure how to do this on the server. Maybe check a header flag??
} else {
const flagValue = localStorage.getItem(`isUserLoggedIn`);
// If the flag is null, then the user probably isn't logged in (if you add this to your code, the user will experience a one-time-only bad guess). Otherwise, check the flag's value.
const isLoggedIn = (null !== flagValue) && (flagValue === 'true');
return isLoggedIn;
}
}
const _setFlag = (value: boolean) => {
const valAsString = (value) ? 'true' : 'false';
localStorage.setItem('isUserLoggedIn', valAsString);
}
function useIsLoggedIn() {
const initialGuess = _guessInitialLoginStatus();
let [isLoggedIn, setIsLoggedIn] = useState(initialGuess)
const _updateLoggedInState = (value: boolean) => {
setIsLoggedIn(value);
_setFlag(value);
}
useEffect(() => {
let updateUser = async () => {
try {
await Auth.currentAuthenticatedUser()
_updateLoggedInState(true);
} catch {
_updateLoggedInState(false);
}
}
Hub.listen('auth', updateUser) // listen for login/signup events
// we are not using async to wait for updateUser, so there will be a flash of page where the user is assumed not to be logged in. If we use a flag
updateUser() // check manually the first time because we won't get a Hub event
return () => Hub.remove('auth', updateUser) // cleanup
}, []);
return isLoggedIn;
}
export default useIsLoggedIn;
// header.tsx
import useIsLoggedIn from './useisloggedin';
const Header = () => {
const isLoggedIn = useIsLoggedIn();
return (isLoggedIn) ? <div><button>Sign Out</button></div> :
(<div>
<a href='/signin'>
<button>Login</button>
</a>
</div>);
}
Expected behavior:
The first time the user goes to the site: user is considered not to be logged in (but there is an non-blocking background getCurrentAuthenticatedUser
just to be sure). The very first time that you use this code, you might be logged in but there won't be a cookie, so the initial guess will be a false negative for that first page visit, but this will self-correct when getCurrentAuthenticatedUser
returns after a second.
If the user does not sign in, but they visit the site a second (or subsequent) times -- the localStorage flag will quickly guess that the user is not logged in (and will later be confirmed by getCurrentAuthenticatedUser`).
If the user does sign in on the first visit, then all subsequent visits will check the localStorage flag and quickly determine that the user is signed in. If the flag is wrong, the user's status will update to false when getCurrentAuthenticatedUser
returns after a second. This should be a very rare event -- I think it would only happen if a user's session expires. You could add a timeout to the cookie.
If the user signs in but then logs out, all subsequent visits will check the cache and determine that the user is not signed in.
Potential Issue
Some people might try to use this isLoggedIn
flag before querying currentAuthenticatedUser
which would negate some of the efficiency goals of this flag. Also, this code doesn't cache currentAuthenticatedUser
, which would be nice. Probably the end goal is a hook that caches both the isLoggedIn
flag and the currentAuthenticatedUser
. The only trick there is when isLoggedIn = true
but currentAuthenticatedUser
hasn't returned yet -- so, components would need to be aware of that possibility when deciding how to render user information.
Edit: tested the code and it appears to be a working solution :)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Keeping open for auth v2 milestone
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Keeping open for auth v2 milestone round 2
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Keeping open for auth v2 milestone round 2
I've received another request for amplify auth react hooks today from https://twitter.com/PipoPeperoni
Not stale, please reopen
I've received another request for amplify auth react hooks today from https://twitter.com/PipoPeperoni
Is there any plan for implementing it in Amplify framework?
I've put together a simple custom hook based on the code from @all-iver :)
import Auth from "@aws-amplify/auth";
import { Hub } from "@aws-amplify/core";
import { CognitoUser } from "amazon-cognito-identity-js";
import { useEffect, useState } from "react";
export interface UseAuthHookResponse {
currentUser: CognitoUser | null;
signIn: () => void;
signOut: () => void;
}
const getCurrentUser = async (): Promise<CognitoUser | null> => {
try {
return await Auth.currentAuthenticatedUser();
} catch {
// currentAuthenticatedUser throws an Error if not signed in
return null;
}
};
const useAuth = (): UseAuthHookResponse => {
const [currentUser, setCurrentUser] = useState<CognitoUser | null>(null);
useEffect(() => {
const updateUser = async () => {
setCurrentUser(await getCurrentUser());
};
Hub.listen("auth", updateUser); // listen for login/signup events
updateUser(); // check manually the first time because we won't get a Hub event
return () => Hub.remove("auth", updateUser);
}, []);
const signIn = () => Auth.federatedSignIn();
const signOut = () => Auth.signOut();
return { currentUser, signIn, signOut };
};
export default useAuth;
export { getCurrentUser };
@nielsboecker How am I supposed to import this in App.js? I am trying this way but it doesn't seem to work:
App.js
export default function App() {
const [authState, setAuthState] = React.useState();
const [user, setUser] = React.useState();
const signedIn = useAuth;
useEffect(() => {
console.log(signedIn);
onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData);
});
}, []);
Trying to import this in my components and see if a user is logged in. Since useAuth has "Hub" attached. All of components would know as soon as a user is logged out.
Wha is the use of this in line below? Seems confusing. -->
const [currentUser, setCurrentUser] = useState
@CR1AT0RS, my code snippet is using TypeScript. In TS, useState
can receive generic parameters to specify which type the returned value of the state will be. In our case, it's first going to be null
, and then it will be a CognitoUser
. In other words, <CognitoUser | null>
. If you want to use this in JavaScript, just remove the type information.
In order to consume this hook, you can just destructure currentUser
and don't have to duplicate the low-level state handling in other components:
const { currentUser } = useAuth();
Most helpful comment
@ajhool judging from this and your other recent issue I think you're at the same point I'm at, going through the tutorials and slowly realizing the intended workflow doesn't apply to you. All the methods I could find to do this are async, so there was no obvious way to do this just from a render function. I ended up using the Authenticator component manually (i.e. don't use withAuthenticator unless you're forcing the user to log in), and I have my top-level component provide the user to children as React context.
This code is in my top level component's render function (using React hooks), and then it uses React context to provide the user to its children. This seems to work, although there is probably a small window where you render without having the user, so there's a flash when it updates. I'm just learning Amplify so I don't know if this is the best way to do it.