-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hub.listen
not firing when inside useEffect
#13436
Comments
Hub.listen
not firing signInWithRedirect
eventsHub.listen
not firing when inside useEffect
I'm seeing a similar issue. In my case, I'm trying to listen for the |
@amoffat could be the case. I would imagine this issue is effecting all events rather than just the |
hello @cekpowell . Did you try placing |
Hey @israx - does it specifically have to be the root layout for the app? Mine is a server component and I was under the impression making it a client component would be bad practice. Is this the setup you use? Anyway, I tested it with a |
Hub was design to be used in SPA applications, meaning that Hub events will be dispatched in the same window object where it was initiated. So if Hub is initialized at the top of the element tree, then Amplify APIs called in any children components will be able to trigger the dispatch of events. If you are calling an API on the server, and initializing Hub on the client, that would not work because there are 2 different instances of Hub. |
I'm using Hub client side in an SPA. Here's what I noticed: the |
Thanks @israx. I have tried placing the Hub in my Should the Hub work in this case? I need to listen to auth events so I can update state, and I can only update state inside a |
@amoffat the library will check and emit the |
@cekpowell could you share code snippets and show how you are setting up Hub and what APIs you are using to trigger the dispatch of events ? |
Sure thing: I have an 'use client'
import { decodeCognitoIdToken } from '@/utils/jwt'
import { AppSessionStorage, AppSessionStorageKeys } from '@/utils/sessionStorage'
import { Amplify } from 'aws-amplify'
import {
signInWithRedirect as amplifySignInViaSocial,
AuthError,
fetchAuthSession,
JWT,
} from 'aws-amplify/auth'
import {Hub} from 'aws-amplify/utils'
import { createContext, ReactNode, useCallback, useContext, useState } from 'react'
/**
* * ------------------------------------------------------------------------
* * MARK: Amplify Initialization
* * ------------------------------------------------------------------------
*/
Amplify.configure(AMPLIFY_CONFIG, { ssr: true })
/**
* * ------------------------------------------------------------------------
* * MARK: Types
* * ------------------------------------------------------------------------
*/
/** Supported providers for social sign-in in the app. */
export type AppSocialSignInProviders = 'Google' | 'Facebook' | 'Apple'
export interface SignInSocialParams {
/** Social provider to sign the user into the app with. */
provider: AppSocialSignInProviders
}
export enum SignInViaSocialErrors {
/** Unknown error occured (i.e., error we do not account for). Can also occur if theres no internet connection. */
unknown = 'unknown',
}
/**
* * ------------------------------------------------------------------------
* * MARK: Provider
* * ------------------------------------------------------------------------
*/
export interface AppUserDetails {
/** User's ID */
userId: string
/** User's email */
email: string
/** User's name */
name: string
/** URL to user's profile picture */
picture?: string
/** Access Token (JWT) */
accessToken: JWT
/** idToken (JWT) */
idToken: JWT
}
export interface AuthContextType {
/**
* Data
*/
/** Is there a user currently signed in? */
isSignedIn: boolean
/** `AppUserDetails` for currently signed in user. Undefined if no user is signed in. */
userDetails?: AppUserDetails
/**
* Operations
*/
/** Refreshes the user's auth state. */
refreshAuthState: () => Promise<void>
/** Sign the user in/up to the app via a social provider. */
signInViaSocial: (params: SignInSocialParams) => Promise<void>
}
export const AuthContext = createContext<AuthContextType>({
// data
isSignedIn: false,
userDetails: undefined,
// operations
refreshAuthState: async () => {},
signInViaSocial: async () => {},
})
interface AuthProviderProps {
/** Clear the cache of the `AuthProvider` on intialisation? */
clearCache?: boolean
/** Provider children */
children?: ReactNode
}
/**
* Provides and manages the authentication state of the app.
*/
export const AuthProvider = ({ children }: AuthProviderProps) => {
// getting auth state via `useAuthProvider` hook.
const auth = useAuthProvider()
// provider
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>
}
/**
* Hook that provides and manages the authentication state. We define this hook
* to save defining all of the auth logic inside the `AuthProvider` component.
*/
const useAuthProvider = () => {
/**
* Data
*/
// is the user signed in?
const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
// details for currently signed in user
const [userDetails, setUserDetails] = useState<AppUserDetails | undefined>(undefined)
useEffect(() => {
const unsubscribe = Hub.listen('auth', async ({payload}) => {
console.log(payload)
})
return unsubscribe
}, [])
/**
* Operations
*/
/**
* Refresh Auth Context
*/
/**
* Refreshes the auth context based on the current auth session.
*
* Should be called whenever the users auth state changes and we need to update
* our context - e.g., sign in, sign out, etc.
*/
const refreshAuthState = useCallback(async () => {
// fetching user tokens
const authSession = await fetchAuthSession()
const accessToken = authSession.tokens?.accessToken
const idToken = authSession.tokens?.idToken
const userAttributes = idToken && decodeCognitoIdToken(idToken.toString())
// if user is signed in -> lets update state with user info
if (accessToken && idToken && userAttributes) {
setIsSignedIn(true)
setUserDetails({
userId: userAttributes.preferred_username,
email: userAttributes.email,
name: userAttributes.name,
picture: userAttributes.picture,
accessToken: accessToken,
idToken: idToken,
})
}
// if user is not signed in -> lets clear the state
if (!accessToken || !idToken || !userAttributes) {
setIsSignedIn(false)
setUserDetails(undefined)
}
}, [])
/**
* Sign Up
*/
/**
* Sign in via Social
*/
/**
* Sign the user in/up to the app via a social provider.
*
* **NOTE**: This method will re-direct the user to the social provider's login
* page, and they will then be send back to the app. You must handle this re-direct
* back to the app in order to complete the sign-in/sign-up.
*/
const signInViaSocial = async ({ provider }: SignInSocialParams) => {
try {
// setting the provider into storage (so we can access it after the re-direct)
AppSessionStorage.setItem(AppSessionStorageKeys.ACTIVE_SOCIAL_PROVIDER, provider)
// signing in via social
await amplifySignInViaSocial({
provider: provider,
customState: 'my-custom-state',
})
} catch (e) {
// cleaning up session storage
AppSessionStorage.removeItem(AppSessionStorageKeys.ACTIVE_SOCIAL_PROVIDER)
// throwing custom error
throw new Error(SignInViaSocialErrors.unknown)
}
}
/**
* Returning auth state.
*/
return {
// data
isSignedIn,
userDetails,
// operations
refreshAuthState,
signInViaSocial,
}
}
/**
* * ------------------------------------------------------------------------
* * MARK: Hooks
* * ------------------------------------------------------------------------
*/
/**
* Returns the Auth context for the app.
*/
export const useAuth = () => {
return useContext(AuthContext)
} This const RootLayout = async ({
children,
}: Readonly<{
children: React.ReactNode
}>) => {
// fetching app locale using next-intl
const locale = await getLocale()
return (
<html lang={locale} suppressHydrationWarning>
{/* remove default margin */}
<body style={{ margin: 0 }}>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
)
}
export default RootLayout Finally, in my Sign In screen, I am calling the |
Gotcha, so my Hub issue is unrelated to this issue then. I'll open the feature request. |
Yes you can signIn in a different tab as long as |
@cekpowell, did the comments from @israx help unblock or clear the issue up here? Let us know if there is anything else that we can help with. |
Hi @cwomack - unfortunately not - this issue is still blocking me, and I have resulted to using a combination of a Are you able to confirm if this is in-fact a bug with the SDK? It seems like a very crucial part of functionality that isn't working properly. |
Hi, I hope my comments are useful as this is new to me.
Hope this helps! P.S. This turned out to be a caching problem; clearing the "token" cache in the browser, resolved the problem and to be fair the video explains this ... |
The same issue is happening to me with the same setup. Has this been verified as a real bug, or are we doing something wrong with the setup? I was blocked for a few days with this and had to opt for manually handling it. It would be nice to have an official guide, though. |
I've spent a couple of days chasing this down. I see a race condition where the oAuth flow is run before I feel this is worse in SSR apps like Next.js, and more prevalent in production than dev. Typical example code // ConfigureAmplify.tsx
import { useCognitoHub } from './auth';
export ConfigureAmplify () => {
useCognitoHub();
return null;
};
// layout.tsx
const RootLayout = async ({ children }: Readonly<{ children: React.ReactNode;}>) => (
<html lang="en">
<body>
<ConfigureAmplifyClientSide />
<Providers>{children}</Providers>
</body>
</html>
);
};
// auth.ts
export const useCognitoHub = (setIsAuthLoading: (value: boolean) => void) => {
useEffect(() => {
const removeListener = Hub.listen('auth', async ({ payload }) => {
const { event } = payload;
switch (event) {
// DO IMPORTANT TUFF
}
}, []);
return removeListener;
});
}; I'm working around this issue by delaying the oAuth processing till the listener is read. First this patch // NOTE: We need to delay oAuth on the web till we are ready
diff --git a/dist/esm/singleton/Amplify.d.ts b/dist/esm/singleton/Amplify.d.ts
index 24136eff86d96e951991844ec24537c342f13a4a..b96746d046f66b42b8fc77bdc20d71728908af5e 100644
--- a/dist/esm/singleton/Amplify.d.ts
+++ b/dist/esm/singleton/Amplify.d.ts
@@ -34,7 +34,7 @@ export declare class AmplifyClass {
getConfig(): Readonly<ResourcesConfig>;
/** @internal */
[ADD_OAUTH_LISTENER](listener: (authConfig: AuthConfig['Cognito']) => void): void;
- private notifyOAuthListener;
+ notifyOAuthListener(): void;
}
/**
* The `Amplify` utility is used to configure the library.
diff --git a/dist/esm/singleton/Amplify.mjs b/dist/esm/singleton/Amplify.mjs
index 2a19a00a10bb68a0b3d4898b34b58c42f2910928..9db400e5e235c77bc27f0ce3f94b9b5ad93ccced 100644
--- a/dist/esm/singleton/Amplify.mjs
+++ b/dist/esm/singleton/Amplify.mjs
@@ -53,7 +53,7 @@ class AmplifyClass {
event: 'configure',
data: this.resourcesConfig,
}, 'Configure', AMPLIFY_SYMBOL);
- this.notifyOAuthListener();
+ // this.notifyOAuthListener();
}
/**
* Provides access to the current back-end resource configuration for the Library. Then I modify // auth.ts
export const useCognitoHub = (setIsAuthLoading: (value: boolean) => void) => {
useEffect(() => {
const removeListener = Hub.listen('auth', async ({ payload }) => {
const { event } = payload;
switch (event) {
// DO IMPORTANT TUFF
}
});
// NOTE: We have a small hack to remove the listner race condition
// https://github.com/aws-amplify/amplify-js/issues/13436
Amplify.notifyOAuthListener();
return removeListener;
};
},. []);
};
I think long term we need to be able to pass an option to Amplify.configure e.g. `Amplify.configure(outputs, { ssr: true, delayOAuth: true }); |
Hi @johnf you are correct about the potential race condition, and thank you for providing this work around. Since In addition, I am interested in what's your opinion on the following: Today Amplify JS fires an OAuth listener as early as possible to complete an OAuth flow. What if we change the approach, instead of a eager listener, we provide a callable API, say |
@HuiSF I'll give moving
I think this would work well. The current default behaviour is probably fine for most folks (e.g. there are no issues in react native due to the nature of the platform), but being able to disable it and use the callable API would be perfect. |
@johnf and @cekpowell, wanted to follow up and see if this issue is still blocking you (or anyone following this). Let us know if there's still assistance needed here, thanks! |
@cwomack I'm not blocked but mainly because I'm working around it with a patch I'm patching the code so I can call notifyOAuthListner myself. Tthe approach suggested by @HuiSF would be a good fix for this. diff --git a/dist/esm/singleton/Amplify.d.ts b/dist/esm/singleton/Amplify.d.ts
index 24136eff86d96e951991844ec24537c342f13a4a..b96746d046f66b42b8fc77bdc20d71728908af5e 100644
--- a/dist/esm/singleton/Amplify.d.ts
+++ b/dist/esm/singleton/Amplify.d.ts
@@ -34,7 +34,7 @@ export declare class AmplifyClass {
getConfig(): Readonly<ResourcesConfig>;
/** @internal */
[ADD_OAUTH_LISTENER](listener: (authConfig: AuthConfig['Cognito']) => void): void;
- private notifyOAuthListener;
+ notifyOAuthListener(): void;
}
/**
* The `Amplify` utility is used to configure the library.
diff --git a/dist/esm/singleton/Amplify.mjs b/dist/esm/singleton/Amplify.mjs
index acaadde4240ac63ec630795f726858b9caec11ac..f7f72f8c678a25f96b3d39add3302deed6b54fd9 100644
--- a/dist/esm/singleton/Amplify.mjs
+++ b/dist/esm/singleton/Amplify.mjs
@@ -53,7 +53,7 @@ class AmplifyClass {
event: 'configure',
data: this.resourcesConfig,
}, 'Configure', AMPLIFY_SYMBOL);
- this.notifyOAuthListener();
+ // this.notifyOAuthListener();
}
/**
* Provides access to the current back-end resource configuration for the Library. |
@johnf, thanks for following up and great to hear the workaround that @HuiSF suggested will do for now. We'll update this issue to be a feature request since the workaround is essentially a departure from how we recommend using |
Before opening, please confirm:
JavaScript Framework
Next.js
Amplify APIs
Authentication
Amplify Version
v6
Amplify Categories
auth
Backend
Other
Environment information
Describe the bug
TLDR
If i place a
Hub.listen
into auseEffect
inside a component, theHub
never fires any events, and so I cannot handle the events and update my state accordingly. If i take theHub
out of theuseEffect
, it correctly fires, but I cannot update component state from outside of theuseEffect
as the component has not yet mounted.Full Description & Context
I have a NextJS app that uses the Amplify SDK to handle Authentication. I am not using the Amplify CLI, but connecting to an existing AWS backend I have defined.
I have implemented sign in via social providers (Google, Apple and Facebook), and I am successfully re-directed to the provider, able to sign in, and be re-directed back to the app. Now, I would like to listen for the redirect auth event in my app so that I can update my system auth state (managed in a context) with the user's details, or handle errors if there are any, and then re-direct the user to the next screen.
As per the docs, I am trying to do this with a
Hub.listen
inside auseEffect
in the page component. However, no event is ever fired when I am re-directed back to the app. If I take theHub.listen
out of auseEffect
, or out of the component all together, then the event is fired, but I am no longer able to update my system context (as this must be done inside a component after mounting).I am guessing the event is not firing inside
useEffect
because the app is unmounted when the redirect takes place, but then this raises the question of how I am meant to listen to re-direct auth events using the hub?I have found this existing issue which has been closed, but recent comments suggest others are still facing this problem.
Expected behavior
The
Hub
listener should fire an event forsignInWithRedirect
.Reproduction steps
Hub
listener which just logs the incoming payload, as done in the docs here. Also, place aHub
listener outside of theuseEffect
.signInWithRedirect
, and when redirected back to the app after sign in, the hub in theuseEffect
will not fire any events, but the one inside theuseEffect
will.Code Snippet
// Put your code below this line.
Log output
aws-exports.js
No response
Manual configuration
No response
Additional configuration
No response
Mobile Device
No response
Mobile Operating System
No response
Mobile Browser
No response
Mobile Browser Version
No response
Additional information and screenshots
No response
The text was updated successfully, but these errors were encountered: