Skip to content

Commit 0fa449c

Browse files
authored
fix(clerk-react): Re-render <UserButton /> when <UserButton /> props change (#5069)
1 parent db6e053 commit 0fa449c

File tree

5 files changed

+145
-3
lines changed

5 files changed

+145
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-react': patch
3+
---
4+
5+
Fix an issue where `<UserButton />` wouldn't update when custom menu item props changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { UserButton } from '@clerk/clerk-react';
2+
import { useContext } from 'react';
3+
import { PageContext, PageContextProvider } from '../PageContext.tsx';
4+
import React from 'react';
5+
6+
function Page1() {
7+
const { counter, setCounter } = useContext(PageContext);
8+
9+
return (
10+
<>
11+
<h1 data-page={1}>Page 1</h1>
12+
<p data-page={1}>Counter: {counter}</p>
13+
<button
14+
data-page={1}
15+
onClick={() => setCounter(a => a + 1)}
16+
>
17+
Update
18+
</button>
19+
</>
20+
);
21+
}
22+
23+
export default function Page() {
24+
const [open, setIsOpen] = React.useState(false);
25+
const [theme, setTheme] = React.useState('light');
26+
const [notifications, setNotifications] = React.useState(false);
27+
const [language, setLanguage] = React.useState('en');
28+
29+
return (
30+
<PageContextProvider>
31+
<UserButton fallback={<>Loading user button</>}>
32+
<UserButton.MenuItems>
33+
<UserButton.Action
34+
label={`Chat is ${open ? 'ON' : 'OFF'}`}
35+
labelIcon={<span>🌐</span>}
36+
onClick={() => setIsOpen(!open)}
37+
/>
38+
<UserButton.Action
39+
label={`Theme: ${theme === 'light' ? '☀️ Light' : '🌙 Dark'}`}
40+
labelIcon={<span>🌐</span>}
41+
onClick={() => setTheme(t => (t === 'light' ? 'dark' : 'light'))}
42+
/>
43+
<UserButton.Action
44+
label={`Notifications ${notifications ? '🔔 ON' : '🔕 OFF'}`}
45+
labelIcon={<span>🌐</span>}
46+
onClick={() => setNotifications(n => !n)}
47+
/>
48+
<UserButton.Action
49+
label={`Language: ${language.toUpperCase()}`}
50+
labelIcon={<span>🌍</span>}
51+
onClick={() => setLanguage(l => (l === 'en' ? 'es' : 'en'))}
52+
/>
53+
<UserButton.Action label={'manageAccount'} />
54+
<UserButton.Action label={'signOut'} />
55+
<UserButton.Link
56+
href={'http://clerk.com'}
57+
label={'Visit Clerk'}
58+
labelIcon={<span>🌐</span>}
59+
/>
60+
61+
<UserButton.Link
62+
href={'/user'}
63+
label={'Visit User page'}
64+
labelIcon={<span>🌐</span>}
65+
/>
66+
67+
<UserButton.Action
68+
label={'Custom Alert'}
69+
labelIcon={<span>🔔</span>}
70+
onClick={() => alert('custom-alert')}
71+
/>
72+
</UserButton.MenuItems>
73+
</UserButton>
74+
</PageContextProvider>
75+
);
76+
}

integration/templates/react-vite/src/main.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import SignUp from './sign-up';
1010
import UserProfile from './user';
1111
import UserProfileCustom from './custom-user-profile';
1212
import UserButtonCustom from './custom-user-button';
13+
import UserButtonCustomDynamicLabels from './custom-user-button/with-dynamic-labels.tsx';
1314
import UserButtonCustomTrigger from './custom-user-button-trigger';
1415
import UserButton from './user-button';
1516
import Waitlist from './waitlist';
@@ -75,6 +76,10 @@ const router = createBrowserRouter([
7576
path: '/custom-user-button',
7677
element: <UserButtonCustom />,
7778
},
79+
{
80+
path: '/custom-user-button-dynamic-labels',
81+
element: <UserButtonCustomDynamicLabels />,
82+
},
7883
{
7984
path: '/custom-user-button-trigger',
8085
element: <UserButtonCustomTrigger />,

integration/tests/custom-pages.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createTestUtils, testAgainstRunningApps } from '../testUtils';
77
const CUSTOM_PROFILE_PAGE = '/custom-user-profile';
88
const CUSTOM_BUTTON_PAGE = '/custom-user-button';
99
const CUSTOM_BUTTON_TRIGGER_PAGE = '/custom-user-button-trigger';
10+
const CUSTOM_BUTTON_DYNAMIC_LABELS_PAGE = '/custom-user-button-dynamic-labels';
1011

1112
async function waitForMountedComponent(
1213
component: 'UserButton' | 'UserProfile',
@@ -320,5 +321,59 @@ testAgainstRunningApps({ withPattern: ['react.vite.withEmailCodes'] })(
320321
await pendingDialog;
321322
});
322323
});
324+
325+
test.describe('User Button with dynamic labels', () => {
326+
test('click Chat is OFF and ensure that state has been changed', async ({ page, context }) => {
327+
const u = createTestUtils({ app, page, context });
328+
await u.po.signIn.goTo();
329+
await u.po.signIn.waitForMounted();
330+
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
331+
await u.po.expect.toBeSignedIn();
332+
333+
await u.page.goToRelative(CUSTOM_BUTTON_DYNAMIC_LABELS_PAGE);
334+
await u.po.userButton.waitForMounted();
335+
await u.po.userButton.toggleTrigger();
336+
await u.po.userButton.waitForPopover();
337+
338+
const pagesContainer = u.page.locator('div.cl-userButtonPopoverActions__multiSession').first();
339+
const buttons = await pagesContainer.locator('button').all();
340+
341+
expect(buttons.length).toBe(9);
342+
343+
const expectedTexts = [
344+
'🌐Chat is OFF',
345+
'🌐Theme: ☀️ Light',
346+
'🌐Notifications 🔕 OFF',
347+
'🌍Language: EN',
348+
'Manage account',
349+
'Sign out',
350+
'🌐Visit Clerk',
351+
'🌐Visit User page',
352+
'🔔Custom Alert',
353+
];
354+
355+
for (let i = 0; i < buttons.length; i++) {
356+
await expect(buttons[i]).toHaveText(expectedTexts[i]);
357+
}
358+
359+
const chatButton = buttons[0];
360+
const notificationsButton = buttons[2];
361+
const languageButton = buttons[3];
362+
363+
// Test chat toggle
364+
await chatButton.click();
365+
await u.po.userButton.toggleTrigger();
366+
await u.po.userButton.waitForPopover();
367+
await expect(chatButton).toHaveText('🌐Chat is ON');
368+
await expect(languageButton).toHaveText('🌍Language: EN');
369+
370+
await notificationsButton.click();
371+
await u.po.userButton.toggleTrigger();
372+
await u.po.userButton.waitForPopover();
373+
await expect(notificationsButton).toHaveText('🌐Notifications 🔔 ON');
374+
await expect(chatButton).toHaveText('🌐Chat is ON');
375+
await expect(languageButton).toHaveText('🌍Language: EN');
376+
});
377+
});
323378
},
324379
);

packages/react/src/components/ClerkHostRenderer.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ export class ClerkHostRenderer extends React.PureComponent<
6363

6464
// Remove children and customPages from props before comparing
6565
// children might hold circular references which deepEqual can't handle
66-
// and the implementation of customPages or customMenuItems relies on props getting new references
67-
const prevProps = without(_prevProps.props, 'customPages', 'customMenuItems', 'children');
68-
const newProps = without(this.props.props, 'customPages', 'customMenuItems', 'children');
66+
// and the implementation of customPages relies on props getting new references
67+
const prevProps = without(_prevProps.props, 'customPages', 'children');
68+
const newProps = without(this.props.props, 'customPages', 'children');
69+
6970
// instead, we simply use the length of customPages to determine if it changed or not
7071
const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length;
7172
const customMenuItemsChanged = prevProps.customMenuItems?.length !== newProps.customMenuItems?.length;

0 commit comments

Comments
 (0)