Skip to content

Commit 44b10e2

Browse files
refactor: use namespace React import and retain error in useAuth0Suspense
1 parent acc9ba4 commit 44b10e2

3 files changed

Lines changed: 70 additions & 45 deletions

File tree

EXAMPLES.md

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,43 +23,6 @@
2323
- [Session Expiry from Upstream IdP (IPSIE)](#session-expiry-from-upstream-idp-ipsie)
2424
- [Use Suspense for loading state (React 19+)](#use-suspense-for-loading-state-react-19)
2525

26-
## Use Suspense for loading state (React 19+)
27-
28-
On React 19 and later, `useAuth0Suspense` lets a `<Suspense>` boundary handle the
29-
auth loading state and an Error Boundary handle initialization errors, so your
30-
component code stays focused on rendering. Unlike `useAuth0`, it does not return
31-
`isLoading` (the component suspends until auth is ready) or `error` (it is thrown
32-
to the nearest Error Boundary).
33-
34-
```jsx
35-
import { Suspense } from 'react';
36-
import { Auth0Provider, useAuth0Suspense } from '@auth0/auth0-react';
37-
38-
function App() {
39-
return (
40-
<Auth0Provider
41-
domain="YOUR_DOMAIN"
42-
clientId="YOUR_CLIENT_ID"
43-
authorizationParams={{ redirect_uri: window.location.origin }}
44-
>
45-
<MyErrorBoundary fallback={<p>Could not sign you in.</p>}>
46-
<Suspense fallback={<p>Loading...</p>}>
47-
<UserGreeting />
48-
</Suspense>
49-
</MyErrorBoundary>
50-
</Auth0Provider>
51-
);
52-
}
53-
54-
function UserGreeting() {
55-
const { user, isAuthenticated } = useAuth0Suspense();
56-
return isAuthenticated ? <p>Hello, {user?.name}!</p> : <p>Please log in</p>;
57-
}
58-
```
59-
60-
`useAuth0Suspense` requires React 19; calling it on an earlier version throws a
61-
clear error. All the auth methods available on `useAuth0` (`loginWithRedirect`,
62-
`logout`, `getAccessTokenSilently`, etc.) are also available here.
6326

6427
## Use with a Class Component
6528

@@ -1910,3 +1873,41 @@ function CallApi() {
19101873
```
19111874
19121875
Using `withAuthenticationRequired` on protected routes is the simpler alternative — the redirect happens automatically without the null check.
1876+
1877+
1878+
## Use Suspense for loading state (React 19+)
1879+
1880+
On React 19 and later, `useAuth0Suspense` lets a `<Suspense>` boundary handle the
1881+
auth loading state and an Error Boundary handle initialization errors, so your
1882+
component code stays focused on rendering. Unlike `useAuth0`, it does not return
1883+
`isLoading` — the component suspends until auth is ready.
1884+
1885+
```jsx
1886+
import { Suspense } from 'react';
1887+
import { Auth0Provider, useAuth0Suspense } from '@auth0/auth0-react';
1888+
1889+
function App() {
1890+
return (
1891+
<Auth0Provider
1892+
domain="YOUR_DOMAIN"
1893+
clientId="YOUR_CLIENT_ID"
1894+
authorizationParams={{ redirect_uri: window.location.origin }}
1895+
>
1896+
<MyErrorBoundary fallback={<p>Could not sign you in.</p>}>
1897+
<Suspense fallback={<p>Loading...</p>}>
1898+
<UserGreeting />
1899+
</Suspense>
1900+
</MyErrorBoundary>
1901+
</Auth0Provider>
1902+
);
1903+
}
1904+
1905+
function UserGreeting() {
1906+
const { user, isAuthenticated } = useAuth0Suspense();
1907+
return isAuthenticated ? <p>Hello, {user?.name}!</p> : <p>Please log in</p>;
1908+
}
1909+
```
1910+
1911+
`useAuth0Suspense` requires React 19; calling it on an earlier version throws a
1912+
clear error. All the auth methods available on `useAuth0` (`loginWithRedirect`,
1913+
`logout`, `getAccessTokenSilently`, etc.) are also available here.

__tests__/use-auth0-suspense.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@ describe('useAuth0Suspense', () => {
106106
);
107107
});
108108

109+
it('returns the auth methods, omitting isLoading and _initPromise', async () => {
110+
clientMock.checkSession.mockResolvedValue(undefined);
111+
clientMock.getUser.mockResolvedValue({ name: 'Bob' });
112+
113+
let captured: Record<string, unknown> | undefined;
114+
function Capture() {
115+
captured = useAuth0Suspense() as unknown as Record<string, unknown>;
116+
return <div>captured</div>;
117+
}
118+
119+
await renderWithProvider(<Capture />);
120+
await waitFor(() =>
121+
expect(screen.getByText('captured')).toBeInTheDocument()
122+
);
123+
124+
expect(captured).not.toHaveProperty('isLoading');
125+
expect(captured).not.toHaveProperty('_initPromise');
126+
expect(captured).toHaveProperty('error');
127+
expect(typeof captured!.loginWithRedirect).toBe('function');
128+
});
129+
109130
it('throws a clear error when used outside an Auth0Provider', () => {
110131
expect(() => renderHook(() => useAuth0Suspense())).toThrowError(
111132
/must be used within/

src/use-auth0-suspense.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { use, useContext } from 'react';
1+
// Namespace import: `use` only exists as a named export from React 19, so
2+
// `import { use }` fails at link time for React 16-18 consumers even if they
3+
// never call this hook. Property access stays late-bound.
4+
import * as React from 'react';
25
import { User } from '@auth0/auth0-spa-js';
36
import Auth0Context, { Auth0ContextInterface } from './auth0-context';
47

58
/**
69
* The value returned by `useAuth0Suspense`: the full `useAuth0` interface minus
7-
* `isLoading` (always resolved by the time the hook returns) and `error`
8-
* (thrown to the nearest Error Boundary instead of returned).
10+
* `isLoading` and the internal `_initPromise`. `error` is
11+
* retained for post-init failures such as `loginWithPopup`.
912
*/
1013
export type Auth0SuspenseContextInterface<TUser extends User = User> = Omit<
1114
Auth0ContextInterface<TUser>,
12-
'isLoading' | 'error'
15+
'isLoading' | '_initPromise'
1316
>;
1417

1518
/**
@@ -34,13 +37,13 @@ export type Auth0SuspenseContextInterface<TUser extends User = User> = Omit<
3437
const useAuth0Suspense = <TUser extends User = User>(
3538
context = Auth0Context
3639
): Auth0SuspenseContextInterface<TUser> => {
37-
if (typeof use !== 'function') {
40+
if (typeof React.use !== 'function') {
3841
throw new Error(
3942
'useAuth0Suspense requires React 19 or later (React.use is unavailable).'
4043
);
4144
}
4245

43-
const ctx = useContext(context) as Auth0ContextInterface<TUser>;
46+
const ctx = React.useContext(context) as Auth0ContextInterface<TUser>;
4447

4548
if (!ctx._initPromise) {
4649
throw new Error(
@@ -49,10 +52,10 @@ const useAuth0Suspense = <TUser extends User = User>(
4952
}
5053

5154
// Suspends until the init promise resolves; re-throws if it rejected.
52-
use(ctx._initPromise);
55+
React.use(ctx._initPromise);
5356

5457
// eslint-disable-next-line @typescript-eslint/no-unused-vars
55-
const { isLoading, error, ...rest } = ctx;
58+
const { isLoading, _initPromise, ...rest } = ctx;
5659
return rest;
5760
};
5861

0 commit comments

Comments
 (0)