amplify-ui: useAuthenticator doesn't trigger re-render when using at the app level

Before creating a new issue, please confirm:

On which framework/platform are you having an issue?

React

Which UI component?

Authenticator

How is your app built?

Create React App

Please describe your bug.

I am trying to use the useAuthenticator hook to set a global authentication state.

However, several problems have arisen:

  1. When I use const { user, signOut } = useAuthenticator((context) => [context.user]);, as is mentioned here, there is no app-level re-rendering. So several other components that are also using the hook and are dependent on authentication state don’t change.
  2. If I refresh the page, React renders everything as signed out. When I go to sign in, it then signs me in automatically without me having to manually sign in. It should ideally render everything as signed in from the start, since I am already signed in.

What’s the expected behaviour?

When I sign in or out, React should trigger an app-level re-render with the new authentication state. Furthermore, if I refresh the page, the authentication state should not change.

Help us reproduce the bug!

Following the instructions, I added Authenticator.Provider at the app level:

ReactDOM.render(
  <Authenticator.Provider>
    <HelmetProvider>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </HelmetProvider>
  </Authenticator.Provider>,
  document.getElementById('root')
);

I use useAuthenticator to Login:

const { route } = useAuthenticator(context => [context.route]);
    return (
        route === 'authenticated' ? <Navigate to="/dashboard/app" />: (
            <Authenticator
                // Default to Sign Up screen
                initialState="signUp"
                // Customize `Authenticator.SignUp.FormFields`
                signUpAttributes={['preferred_username', 'birthdate']}
                components={components}
                services={{
                    async validateCustomSignUp(formData) {
                        if (!formData.acknowledgement) {
                            return {
                                acknowledgement: 'You must agree to the Terms & Conditions',
                            };
                        }
                    },
                }}
            />
        )
            );

I define const { user, signOut } = useAuthenticator((context) => [context.user]); in components, not at the app level, where I want to obtain the authentication state. Following the guide, I use the conditional typeof user === 'undefined' to see if the user is authenticated.

Code Snippet

// Put your code below this line.

Additional information and screenshots

No response

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 5
  • Comments: 53 (22 by maintainers)

Most upvoted comments

It baffles my mind that this has not been fixed. The reason we are trying to use Authentication.Provider and useAuthenticator in our application is because if we create a custom login/sign-up form without it, it appears there is no way to automatically sign-in the user after they confirm their email without using these. Even using them, it’s unclear whether this is possible.

https://github.com/aws-amplify/amplify-js/issues/2562

If I use my own custom components and call Auth methods manually, I have to have the user login again after confirmation (ruins user experience and onboarding) or temporarily store the login credentials in the browser as mentioned in the issue above.

I don’t understand why there is even documentation for a headless usage of Authenticator if it doesn’t respond to Auth.signIn events and only responds to Auth.signOut events. It led me down a rabbit hole of developer pain and in my opinion, this lack of functionality makes it completely useless.

In addition, there appears to be no way to trigger a transition to verifyUser so that I can use the sendForm internal method in the workaround discussed above.

It also appears nonsensical to me how there can be all of these different authStates but there are only transitions available for just a few of them. If this is the case, how am I meant to manually transition to any of the others?

This is not an advanced use case. This is a use case that tons of developers will have. Assuming developers will just lightly modify the slots or CSS variables for the Authenticator is crazy. Obviously developers will want to build their own custom authentication UI components. In my case, I just have a custom form and want to use that to perform authentication instead of the pre-built UI.

It’s quite unbelievable that this has not yet been fixed after 1 year! Come on guys, lots of devs rely on this stuff.

It does seem to be working for me now @reesscot! Still having some issues, but I think they may be on my end, elsewhere in the code. In console logs, I’m seeing updated values for user and authState as expected from useAuthenticator after signIn. Previously, I wasn’t seeing updated values. Thanks!

One thing I didn’t explicitly check is autoSignIn, which I think utilizes a different Hub event. We don’t need it for our current use case, but would be good to ensure that it supported as well.

Either way, thanks for digging in. Much appreciated!

@kyokosdream It is now possible to Automatically sign in using the JS Auth API’s. See https://docs.amplify.aws/lib/auth/emailpassword/q/platform/js/#auto-sign-in-after-sign-up

We are working on supporting your use cases above, and will update this ticket when we have more detailed plan.

Ok, I can confirm that using only <Authenticator.Provider> with aws-amplify/auth methods works pretty well with useAuthenticator for a fully custom UI experience. Here’s the simple implementation that works with no force window reload to re-render etc. for login or logout needed and useAuthenticator hook works beautifully

React+Vite

App.jsx

const Login = React.lazy(() => import('./pages/Authentication/Login'))
const SignUp = React.lazy(() => import('./pages/Authentication/Signup'))
const Reset = React.lazy(() => import('./pages/Authentication/Reset'))

function App() {
  const { authStatus } = useAuthenticator((context) => [context.user]);
  
  return (
    <>
    {
      (authStatus === "authenticated")?
      <Routes>
        <Route path="/" element={<AppLayout />}>
          .....//all the app authenticated routes
        </Route>
      </Routes>
      :<Routes>       
          <Route path="/" element={<Authenticate />}>
              <Route index element={<React.Suspense fallback={<Spin/>}>
                                      <Login/> //--> fully custom UI using aws-amplify/auth
                                    </React.Suspense>} />
              <Route path="signup" element={<React.Suspense fallback={<Spin/>}>
                                    <SignUp/>  //--> fully custom UI using aws-amplify/auth
                                  </React.Suspense>} />   
              <Route path="reset" element={<React.Suspense fallback={<Spin/>}>
                                    <Reset/>  //--> fully custom UI using aws-amplify/auth
                                  </React.Suspense>} /> 
          </Route>   
      </Routes>
    }
    </>
  )
}

main.jsx

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Authenticator.Provider>
        <BrowserRouter>
          <App />
        </BrowserRouter>
    </Authenticator.Provider>
  </React.StrictMode>,
)

AppLayout.jsx (has SignOut button)

const { signOut } = useAuthenticator((context) => [context.user]);

const onSignOut = () => {
        signOut()      
}

return (<button onClick={onSignOut}>Log out</button>)

Actually quite shocked to see how well this works.

I had much better luck with the Authenticator component and provider when I was using amplify-js version 5. On version 6, not as much luck. I found a workaround that may help others.

I only use authStatus from useAuthenticator. This seems to update to “authenticated” upon login using the Authenticator component, but I could not get it to update to “unauthenticated” upon logout.

I found a workaround for that issue.

I’m also using redux, and at signOut, I do not use the signOut method from useAuthenticator. Instead, I use:

import { signOut } from "aws-amplify/auth";

And my sign-out button handler then does this:

    void signOut();
    dispatch(setLogoutInt(randomLargeInt()));

setLogoutInt is a redux action, and dispatch is the redux Dispatch.

Then, this was the trick that got things working for me: forcing a rerender of the Provider upon logout via redux:

  const logoutInt = useAppSelector(selectLogoutInt);
  return (
    <Authenticator.Provider key={logoutInt}>
      <AppBeneathAuthenticatorProvider />
    </Authenticator.Provider>
  );

With this workaround, I am able to use amplify v6 with the Authenticator component and logout successfully. Hope it helps! Good luck.

@calebpollman

Generally, the documentation is good, but since time has changed the capabilities it seems like it is a bit dated.

I’m using React Native but this approach also works with React or JavaScript.

Firstly, the documentation relies on examples. There isn’t the classic documentation style that outlines each property, class or method that is available on each hook or module. This would already be helpful and probably easier to maintain. When something is deprecated the page that the information is displayed can also say it has been deprecated. This is relatively common when reviewing documentation for other APIs.

Secondly, currently the AWS Amplify customization docs suggest this approach but this is not quite right because the NBM "module itself seems to handle the revealing of the children (see my earlier post’s “Other finds” section).

Lastly, if you search for “handlesubmit” on the authenticator documentation page the word doesn’t exist Screenshot 2024-01-13 at 2 54 27 PM It would at the bare minimum be helpful to know that this exists as a prop when instantiating a custom “Sign In” view.

For an example, you can go with the a basic approach, which satisfies almost all use-cases.

Use case: I want to add custom authentication to my app. Result:

Provider

<Authenticator.Provider>
  <Authenticator
    components={{
      SignIn: props => {
        return <MySignIn {...props} />;
      },
      SignUp: props => {
        return <MySignUp {...props} />;
      },
    }}
    Header={MyHeader}
    Footer={Footer}
  >
    {children}
  </Authenticator>
</Authenticator.Provider>

MySignIn component

const MySignIn: React.FC<any> = ({ toSignUp, handleSubmit }) => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSignIn = async () => {
    try {
      await handleSubmit({ username, password });
    } catch (error) {
      console.error('Error signing in:', error);
    }
  };
  return (
    <View
      style={{
        marginTop: 24,
        padding: 24,
      }}
    >
      <TextInterBold style={[Styles.h2, { marginBottom: 30 }]}>
        Sign in
      </TextInterBold>
      <View
        style={{
          marginBottom: 30,
        }}
      >
        <Input
          style={Styles.input}
          placeholder="Username"
          onChangeText={text => setUsername(text)}
          value={username}
          autoComplete="email"
        />
        <Input
          style={Styles.input}
          placeholder="Password"
          secureTextEntry={true}
          onChangeText={text => setPassword(text)}
          value={password}
          autoComplete="password"
        />
      </View>
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-between',
          marginTop: 10,
          marginBottom: 30,
        }}
      >
        <Button
          style={[Styles.button, { width: '25%' }]}
          onPress={handleSignIn}
        >
          Sign In
        </Button>
        <Button style={[Styles.button, { width: '25%' }]} onPress={toSignUp}>
          Create an account
        </Button>
      </View>
    </View>
  );
};

export default MySignIn;

Note the use of handleSubmit versus what the documentation suggests with is signIn()

You can see the documentation suggests using signIn() here. and in several other places for JavaScript or React. This is misleading as we both know that this doesn’t work as expected.

I would suggest including information at this point in the documentation that explains the use of handleSubmit, even if this is a temporary alternative and a fix is coming in the future. Because for people like myself I almost had to forego using AWS Amplify due to the lack of ability to log a user in with a custom form. That is probably not ideal for AWS Amplify.

I hope this helps and wish you luck improving the docs. Thanks!

@adriaanbalt Glad that you got things working!

Question: Why is this information so difficult to find and not an obvious alternative?

The documentation could be improved here, think the addition of a concrete example for this use case would be a good starting point but curious if there is anything else that you would have found helpful?

@adriaanbalt That was a mistake on my end. It’s available as a prop passed in to the SignIn component:

components={ SignIn: ({ handleSubmit, ...props }) => {
  // ...do custom UI stuff
  }
}

Hey @adriaanbalt. Are you calling signIn directly from inside MySignIn?

Would love to learn more about why the default useAuthenticator is not working for you

@reesscot I’ll add to this – one of the ways in which useAuthenticator seems to be not working as I would expect is in regards to “headless” usage of Authenticator.Provider, with a custom UI (in my case, via Auth.signUp).

This earlier answer from @ErikCH was really helpful – this seems to be the exact issue I’m running into: (emphasis mine)

If you’re using Auth.signIn, that will not propagate to useAuthenticator or the auth state. You are correct.

The only thing we listen for is Auth.signOut. That will propagate. We are looking at adding more hub events to listen to, so you can interact directly with the JS library, outside of the Authenticator.

Can you please confirm that this is indeed still an issue? If so, it seems the “roll your own provider” approach is the best way forward for now? Is there any plan to continue to add support for the additional hub events propagating to useAuthenticator?

EDIT: to clarify – I’m not doing any page refreshes, it is a single-page app – so the issue is purely that after successfully executing Auth.signUp (either with auto-login, or without auto-login followed by an explicit call to Auth.signIn), useAuthenticator is not aware of the updated, logged-in state.

I ran into this as well, especially if I refreshed the application. I ended up putting together a quick user provider of my own:

import { createContext, useState, useEffect } from 'react';
import { Hub, Auth } from 'aws-amplify';

export const UserContext = createContext();

export const UserProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const signOut = () => Auth.signOut();

    useEffect(() => {
        // Check the current user when the app loads
        Auth.currentAuthenticatedUser().then(user => setUser(user)).catch(() => console.log('Not signed in'));

        // Listen for changes to the Auth state and set the local state
        const hubListenerCancelToken = Hub.listen('auth', (data) => {
            const { payload } = data;
            console.log('A new auth event has happened: ', data.payload.event);
            onAuthEvent(payload);
        });

        return () => {
            hubListenerCancelToken();
        }
    }, []);

    const onAuthEvent = (payload) => {
        switch (payload.event) {
            case 'signIn':
                return setUser(payload.data);
            case 'signOut':
                return setUser(null);
            default:
                return;
        }
    }

    return (
        <UserContext.Provider value={{ user, signOut }}>
            {children}
        </UserContext.Provider>
    );
}

Then I use it where ever I need user info like a top menu or anything else:

import { useContext } from 'react';
import { UserContext } from '../components/UserContext';

function TopMenuBar() {
    const { user, signOut } = useContext(UserContext);
    ....

That works for both reloading and auth changes. I have a separate Login page where I just use the Authenticator and my own context to check if I should redirect back:

import { useEffect, useContext } from "react";

import { UserContext } from "../components/UserContext";

import { Authenticator, View } from "@aws-amplify/ui-react";
import '@aws-amplify/ui-react/styles.css';

import { useNavigate, useLocation } from "react-router-dom";

export default function Login() {
    const { user } = useContext(UserContext);
    const location = useLocation();
    const navigate = useNavigate();
    const from = location.state?.from || "/";

    useEffect(() => {
        if(user) {
            navigate(from, { replace: true });
        }
    }, [user, navigate, from]);

    return (
        <View className="auth-wrapper">
            <Authenticator />
        </View>
    );
}

Hope this helps someone in the meantime!

Hi @silberistgold !

If you’re using Auth.signIn, that will not propagate to useAuthenticator or the auth state. You are correct.

The only thing we listen for is Auth.signOut. That will propagate. We are looking at adding more hub events to listen to, so you can interact directly with the JS library, outside of the Authenticator.

The submitForm is an internal event name, and it’s not mentioned in our documentation. It’s used for both sending the signUp and signIn information. Just beware, you need to be on the correct route, for it to work. You can use the toSignUp or toSignIn to get to that.

With all that said, I wouldn’t recommend using it in the long term, since it could change. However, we are looking into adding better documentation and more utilities to make creating your own headless Authenticator a better experience for advanced used cases like yours.

Hi @vymao !

Yes, so we made a change with #1580 that improves the experiences for users that are on multiple routes. So now as long as you have your application surrounded by Authenticator.Provider the useAuthenticator will work on any route you’re on. Before, if you didn’t have an Authenticator on your page it wouldn’t work.

There is still one more outstanding issue with this solution. On refresh the route will temporarily be in a setup or idle state before it transitions to an authenticated state. If you check route as soon as the page loads, and redirect somewhere while it’s in the idle or setup state, that could be an issue.

I created a guide on authenticated routes here. In it I describe this scenario, and work around for it.

In this scenario if someone goes to an authenticated route, and it’s an idle or setup state then we redirect back to /login. Which will then by that time see the user is authenticated and will re-route back to the authenticated page.

Let me know if that’s what you’re experiencing and if this work around helps in the mean time.