Skip to content

Commit

Permalink
Merge pull request #229 from auth0/feature/use-user-error-handling
Browse files Browse the repository at this point in the history
Added error handling to useUser [SDK-2236]
  • Loading branch information
adamjmcgrath authored Jan 7, 2021
2 parents 71b61ec + 6ceda64 commit d1d9bf3
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 68 deletions.
3 changes: 2 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ Check the user's authentication state and log them in or out from the front end
import { useUser } from '@auth0/nextjs-auth0';

export default () => {
const { user, isLoading } = useUser();
const { user, error, isLoading } = useUser();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;

if (user) {
return (
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,10 @@ Check whether a user is authenticated by checking that `user` has a value, and l
import { useUser } from '@auth0/nextjs-auth0';

export default () => {
const { user, isLoading } = useUser();
const { user, error, isLoading } = useUser();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;

if (user) {
return (
Expand Down
23 changes: 15 additions & 8 deletions examples/basic-example/pages/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@ import { useUser } from '@auth0/nextjs-auth0';
import Layout from '../components/layout';

export default function Home() {
const { user, isLoading } = useUser();
const { user, error, isLoading } = useUser();

return (
<Layout>
<h1>Next.js and Auth0 Example</h1>

{isLoading && <p>Loading login info...</p>}

{!isLoading && !user && (
{error && (
<>
<p>
To test the login click in <i>Login</i>
</p>
<p>
Once you have logged in you should be able to click in <i>Protected Page</i> and <i>Logout</i>
</p>
<h4>Error</h4>
<pre>{error.message}</pre>
</>
)}

Expand All @@ -29,6 +25,17 @@ export default function Home() {
<pre id="profile">{JSON.stringify(user, null, 2)}</pre>
</>
)}

{!isLoading && !error && !user && (
<>
<p>
To test the login click in <i>Login</i>
</p>
<p>
Once you have logged in you should be able to click in <i>Protected Page</i> and <i>Logout</i>
</p>
</>
)}
</Layout>
);
}
13 changes: 10 additions & 3 deletions examples/basic-example/pages/protected-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@ import { useUser, withPageAuthRequired } from '@auth0/nextjs-auth0';
import Layout from '../components/layout';

export default function ProtectedPage() {
const { user, isLoading } = useUser();
const { user, error, isLoading } = useUser();

return (
<Layout>
<h1>Protected Page</h1>

{isLoading && <p>Loading profile...</p>}

{!isLoading && user && (
{error && (
<>
<p>Profile:</p>
<h4>Error</h4>
<pre>{error.message}</pre>
</>
)}

{user && (
<>
<h4>Profile</h4>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)}
Expand Down
23 changes: 15 additions & 8 deletions examples/kitchen-sink-example/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@ import { useUser } from '@auth0/nextjs-auth0';
import Layout from '../components/layout';

export default function Home(): React.ReactElement {
const { user, isLoading } = useUser();
const { user, error, isLoading } = useUser();

return (
<Layout>
<h1>Next.js and Auth0 Example</h1>

{isLoading && <p>Loading login info...</p>}

{!isLoading && !user && (
{error && (
<>
<p>
To test the login click in <i>Login</i>
</p>
<p>
Once you have logged in you should be able to click in <i>Profile</i> and <i>Logout</i>
</p>
<h4>Error</h4>
<pre>{error.message}</pre>
</>
)}

Expand All @@ -29,6 +25,17 @@ export default function Home(): React.ReactElement {
<pre id="profile">{JSON.stringify(user, null, 2)}</pre>
</>
)}

{!isLoading && !error && !user && (
<>
<p>
To test the login click in <i>Login</i>
</p>
<p>
Once you have logged in you should be able to click in <i>Protected Page</i> and <i>Logout</i>
</p>
</>
)}
</Layout>
);
}
13 changes: 10 additions & 3 deletions examples/kitchen-sink-example/pages/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@ import { useUser, withPageAuthRequired } from '@auth0/nextjs-auth0';
import Layout from '../components/layout';

export default withPageAuthRequired(function Profile(): React.ReactElement {
const { user, isLoading } = useUser();
const { user, error, isLoading } = useUser();

return (
<Layout>
<h1>Profile</h1>

{isLoading && <p>Loading profile...</p>}

{!isLoading && user && (
{error && (
<>
<p>Profile:</p>
<h4>Error</h4>
<pre>{error.message}</pre>
</>
)}

{user && (
<>
<h4>Profile</h4>
<pre id="profile">{JSON.stringify(user, null, 2)}</pre>
</>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as UserProvider, UserProfile, UserContext, useUser } from './use-user';
export { default as UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './use-user';
export { default as withPageAuthRequired, WithPageAuthRequired } from './with-page-auth-required';
25 changes: 16 additions & 9 deletions src/frontend/use-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface UserProfile {
*/
export interface UserContext {
user?: UserProfile;
error?: Error;
isLoading: boolean;
}

Expand All @@ -41,7 +42,7 @@ export interface UserContext {
* export default function App({ Component, pageProps }) {
* // If you've used `withAuth`, pageProps.user can pre-populate the hook
* // if you haven't used `withAuth`, pageProps.user is undefined so the hook
* // fetches the user from the API routes
* // fetches the user from the API route
* const { user } = pageProps;
*
* return (
Expand All @@ -57,7 +58,7 @@ export interface UserContext {
*
* @category Client
*/
type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string }>;
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string }>;

/**
* @ignore
Expand All @@ -74,9 +75,10 @@ const User = createContext<UserContext>({ isLoading: false });
* import { useUser } from '@auth0/nextjs-auth0`;
*
* export default function Profile() {
* const { user, isLoading } = useUser();
* const { user, error, isLoading } = useUser();
*
* if (isLoading) return <div>Loading...</div>;
* if (error) return <div>{error.message}</div>;
* if (!user) return <Link href="/api/auth/login"><a>Login</a></Link>;
* return <div>Hello {user.name}, <Link href="/api/auth/logout"><a>Logout</a></Link></div>;
* }
Expand Down Expand Up @@ -104,19 +106,24 @@ export default ({
profileUrl = '/api/auth/me'
}: UserProviderProps): ReactElement<UserContext> => {
const [user, setUser] = useState<UserProfile | undefined>(() => initialUser);
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(() => !initialUser);

useEffect((): void => {
if (user) return;

(async (): Promise<void> => {
const response = await fetch(profileUrl);
const result = response.ok ? await response.json() : undefined;

setUser(result);
setIsLoading(false);
try {
const response = await fetch(profileUrl);
setUser(response.ok ? await response.json() : undefined);
setError(undefined);
} catch (_e) {
setError(new Error(`The request to ${profileUrl} failed`));
} finally {
setIsLoading(false);
}
})();
}, [user]);

return <User.Provider value={{ user, isLoading }}>{children}</User.Provider>;
return <User.Provider value={{ user, error, isLoading }}>{children}</User.Provider>;
};
33 changes: 28 additions & 5 deletions src/frontend/with-page-auth-required.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { useUser } from './use-user';
*/
const defaultOnRedirecting = (): JSX.Element => <></>;

/**
* @ignore
*/
const defaultOnError = (): JSX.Element => <></>;

/**
* Options for the withPageAuthRequired Higher Order Component
*
Expand Down Expand Up @@ -43,6 +48,16 @@ export interface WithPageAuthRequiredOptions {
* Render a message to show that the user is being redirected to the login.
*/
onRedirecting?: () => JSX.Element;
/**
* ```js
* withPageAuthRequired(Profile, {
* onError: error => <div>Error: {error.message}</div>
* });
* ```
*
* Render a fallback in case of error fetching the user from the profile API route.
*/
onError?: (error: Error) => JSX.Element;
}

/**
Expand All @@ -66,18 +81,26 @@ export type WithPageAuthRequired = <P extends object>(
const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => {
return function withPageAuthRequired(props): JSX.Element {
const router = useRouter();
const { returnTo = router.asPath, onRedirecting = defaultOnRedirecting, loginUrl = '/api/auth/login' } = options;
const { user, isLoading } = useUser();
const {
returnTo = router.asPath,
onRedirecting = defaultOnRedirecting,
onError = defaultOnError,
loginUrl = '/api/auth/login'
} = options;
const { user, error, isLoading } = useUser();

useEffect(() => {
if (user || isLoading) return;
if ((user && !error) || isLoading) return;

(async (): Promise<void> => {
await router.push(`${loginUrl}?returnTo=${returnTo}`);
})();
}, [user, isLoading, router, loginUrl, returnTo]);
}, [user, error, isLoading]);

if (error) return onError(error);
if (user) return <Component {...props} />;

return user ? <Component {...props} /> : onRedirecting();
return onRedirecting();
};
};

Expand Down
1 change: 1 addition & 0 deletions src/index.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { WithApiAuthRequired } from './helpers';
import { HandleAuth, HandleCallback, HandleLogin, HandleLogout, HandleProfile } from './handlers';
export {
UserProvider,
UserProviderProps,
UserProfile,
UserContext,
useUser,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const handleCallback: HandleCallback = (...args) => getInstance().handleC
export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args);
export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args);

export { UserProvider, UserProfile, UserContext, useUser } from './frontend';
export { UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './frontend';

export {
Config,
Expand Down
27 changes: 16 additions & 11 deletions tests/fixtures/frontend.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { UserProfile, UserProvider } from '../../src';
import { UserProvider, UserProviderProps, UserProfile } from '../../src';

type FetchUserMock = {
ok: boolean;
Expand All @@ -16,16 +16,21 @@ export const user: UserProfile = {
updated_at: null
};

export const withUser = (user?: UserProfile): React.ComponentType => {
return (props: any): React.ReactElement => <UserProvider {...props} user={user} />;
export const withUserProvider = ({ user, profileUrl }: UserProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => <UserProvider {...props} user={user} profileUrl={profileUrl} />;
};

export const fetchUserMock = (): FetchUserMock => ({
ok: true,
json: (): Promise<UserProfile> => Promise.resolve(user)
});
export const fetchUserMock = (): Promise<FetchUserMock> => {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(user)
});
};

export const fetchUserUnsuccessfulMock = (): Promise<FetchUserMock> => {
return Promise.resolve({
ok: false
});
};

export const fetchUserFailureMock = (): FetchUserMock => ({
ok: false,
json: undefined
});
export const fetchUserErrorMock = (): Promise<FetchUserMock> => Promise.reject(new Error('Error'));
Loading

0 comments on commit d1d9bf3

Please sign in to comment.