Skip to content

Commit 8dcfa14

Browse files
committed
feat(ui): lazy-load phone country code data
Move the 582-line countryCodeData module (~8.7KB) out of the ui-common chunk and into its own async chunk (phone-country-data) loaded on demand when PhoneInput renders. - Create countryCodeDataLoader.ts with dynamic import and sync cache - Update phoneUtils.ts to use lazy getters with US fallback - Update useFormattedPhoneNumber.ts to use lazy getters - Update PhoneInput to load data before rendering via useCountryCodeData hook - Exclude countryCodeData from ui-common cache group in rspack config - Add bundlewatch entry for new phone-country-data chunk (10KB budget) - Add loadCountryCodeData() to test files that need country data
1 parent 3983cf8 commit 8dcfa14

14 files changed

Lines changed: 167 additions & 26 deletions

File tree

packages/ui/bundlewatch.config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
{ "path": "./dist/op-plans-page*.js", "maxSize": "3KB" },
3333
{ "path": "./dist/statement-page*.js", "maxSize": "5KB" },
3434
{ "path": "./dist/payment-attempt-page*.js", "maxSize": "4KB" },
35-
{ "path": "./dist/web3-solana-wallet-buttons*.js", "maxSize": "79KB" }
35+
{ "path": "./dist/web3-solana-wallet-buttons*.js", "maxSize": "79KB" },
36+
{ "path": "./dist/phone-country-data*.js", "maxSize": "10KB" }
3637
]
3738
}

packages/ui/rspack.config.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ const common = ({ mode, variant }) => {
9898
module.resource.includes('/components/SignUp')
9999
),
100100
},
101+
/**
102+
* Phone country code data is lazy-loaded via dynamic import
103+
* and excluded from ui-common to keep it in its own async chunk.
104+
*/
105+
phoneCountryData: {
106+
name: 'phone-country-data',
107+
test: module =>
108+
!!(
109+
module instanceof rspack.NormalModule &&
110+
module.resource &&
111+
module.resource.includes('/countryCodeData.')
112+
),
113+
enforce: true,
114+
},
101115
common: {
102116
minChunks: 1,
103117
name: 'ui-common',
@@ -107,7 +121,8 @@ const common = ({ mode, variant }) => {
107121
module instanceof rspack.NormalModule &&
108122
module.resource &&
109123
!module.resource.includes('/components') &&
110-
!module.resource.includes('node_modules')
124+
!module.resource.includes('node_modules') &&
125+
!module.resource.includes('/countryCodeData.')
111126
),
112127
},
113128
defaultVendors: {

packages/ui/src/components/SignIn/__tests__/SignInFactorOne.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { ClerkAPIResponseError, parseError } from '@clerk/shared/error';
22
import type { SignInResource } from '@clerk/shared/types';
33
import { waitFor } from '@testing-library/react';
4-
import { describe, expect, it, vi } from 'vitest';
4+
import { beforeAll, describe, expect, it, vi } from 'vitest';
55

66
import { bindCreateFixtures } from '@/test/create-fixtures';
77
import { act, mockWebAuthn, render, screen } from '@/test/utils';
8+
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';
89

910
import { SignInFactorOne } from '../SignInFactorOne';
1011

1112
const { createFixtures } = bindCreateFixtures('SignIn');
1213

14+
beforeAll(async () => {
15+
await loadCountryCodeData();
16+
});
17+
1318
describe('SignInFactorOne', () => {
1419
it('renders the component', async () => {
1520
const { wrapper, fixtures } = await createFixtures(f => {

packages/ui/src/components/SignIn/__tests__/utils.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SignInResource } from '@clerk/shared/types';
2-
import { describe, expect, it } from 'vitest';
2+
import { beforeAll, describe, expect, it } from 'vitest';
33

4+
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';
45
import type { FormControlState } from '@/ui/utils/useFormControl';
56

67
import {
@@ -10,6 +11,10 @@ import {
1011
getPreferredAlternativePhoneChannelForCombinedFlow,
1112
} from '../utils';
1213

14+
beforeAll(async () => {
15+
await loadCountryCodeData();
16+
});
17+
1318
describe('determineStrategy(signIn, displayConfig)', () => {
1419
describe('with password as the preferred sign in strategy', () => {
1520
it('selects password if available', () => {

packages/ui/src/components/SignUp/__tests__/SignUpVerifyPhone.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { waitFor } from '@testing-library/react';
2-
import { describe, expect, it } from 'vitest';
2+
import { beforeAll, describe, expect, it } from 'vitest';
33

44
import { bindCreateFixtures } from '@/test/create-fixtures';
55
import { render, screen } from '@/test/utils';
6+
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';
67

78
import { SignUpVerifyPhone } from '../SignUpVerifyPhone';
89

910
const { createFixtures } = bindCreateFixtures('SignUp');
1011

12+
beforeAll(async () => {
13+
await loadCountryCodeData();
14+
});
15+
1116
describe('SignUpVerifyPhone', () => {
1217
it('renders the component', async () => {
1318
const { wrapper } = await createFixtures();

packages/ui/src/components/UserProfile/__tests__/MfaPage.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import type {
66
VerificationJSON,
77
} from '@clerk/shared/types';
88
import { act, waitFor } from '@testing-library/react';
9-
import { afterEach, describe, expect, it, vi } from 'vitest';
9+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
1010

1111
import { bindCreateFixtures } from '@/test/create-fixtures';
1212
import { render } from '@/test/utils';
1313
import { CardStateProvider } from '@/ui/elements/contexts';
14+
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';
1415

1516
import { MfaSection } from '../MfaSection';
1617

18+
beforeAll(async () => {
19+
await loadCountryCodeData();
20+
});
21+
1722
const { createFixtures } = bindCreateFixtures('UserProfile');
1823

1924
const initConfig = createFixtures.config(f => {

packages/ui/src/components/UserProfile/__tests__/PhoneSection.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { act } from '@testing-library/react';
2-
import { describe, expect, it } from 'vitest';
2+
import { beforeAll, describe, expect, it } from 'vitest';
33

44
import { bindCreateFixtures } from '@/test/create-fixtures';
55
import { render, screen } from '@/test/utils';
66
import { CardStateProvider } from '@/ui/elements/contexts';
7+
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';
78

89
import { PhoneSection } from '../PhoneSection';
910

1011
const { createFixtures } = bindCreateFixtures('UserProfile');
1112

13+
beforeAll(async () => {
14+
await loadCountryCodeData();
15+
});
16+
1217
const initConfig = createFixtures.config(f => {
1318
f.withPhoneNumber();
1419
f.withUser({ email_addresses: ['test@clerk.com'] });

packages/ui/src/elements/PhoneInput/__tests__/useFormattedPhoneNumber.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { renderHook, waitFor } from '@testing-library/react';
2-
import { afterEach, describe, expect, it } from 'vitest';
2+
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
33

4+
import { loadCountryCodeData } from '../countryCodeDataLoader';
45
import { useFormattedPhoneNumber } from '../useFormattedPhoneNumber';
56

7+
beforeAll(async () => {
8+
await loadCountryCodeData();
9+
});
10+
611
describe('useFormattedPhoneNumber', () => {
712
afterEach(() => {
813
// Empty the localStorage used within the hook
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type {
2+
CodeToCountryIsoMapType,
3+
CountryEntry,
4+
CountryIso,
5+
IsoToCountryMapType,
6+
} from './countryCodeData';
7+
8+
// Hardcoded US fallback for use before data loads
9+
export const US_FALLBACK_ENTRY: CountryEntry = {
10+
name: 'United States' as CountryEntry['name'],
11+
iso: 'us' as CountryIso,
12+
code: '1' as CountryEntry['code'],
13+
pattern: '(...) ...-....' as CountryEntry['pattern'],
14+
priority: 100,
15+
};
16+
17+
// Module-level cache
18+
let isoToCountryMap: IsoToCountryMapType | undefined;
19+
let codeToCountriesMap: CodeToCountryIsoMapType | undefined;
20+
let subAreaCodeSets: { us: ReadonlySet<string>; ca: ReadonlySet<string> } | undefined;
21+
let loadPromise: Promise<void> | undefined;
22+
23+
export function loadCountryCodeData(): Promise<void> {
24+
if (!loadPromise) {
25+
loadPromise = import(/* webpackChunkName: "phone-country-data" */ './countryCodeData').then(mod => {
26+
isoToCountryMap = mod.IsoToCountryMap;
27+
codeToCountriesMap = mod.CodeToCountriesMap;
28+
subAreaCodeSets = mod.SubAreaCodeSets;
29+
});
30+
}
31+
return loadPromise;
32+
}
33+
34+
export function isCountryCodeDataLoaded(): boolean {
35+
return isoToCountryMap !== undefined;
36+
}
37+
38+
export function getIsoToCountryMap(): IsoToCountryMapType | undefined {
39+
return isoToCountryMap;
40+
}
41+
42+
export function getCodeToCountriesMap(): CodeToCountryIsoMapType | undefined {
43+
return codeToCountriesMap;
44+
}
45+
46+
export function getSubAreaCodeSets() {
47+
return subAreaCodeSets;
48+
}
49+
50+
export type { CountryEntry, CountryIso, IsoToCountryMapType, CodeToCountryIsoMapType };
51+
export type { CountryName, DialingCode, PhonePattern } from './countryCodeData';

packages/ui/src/elements/PhoneInput/index.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useClerk } from '@clerk/shared/react';
2-
import React, { forwardRef, memo, useEffect, useMemo, useRef } from 'react';
2+
import React, { forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react';
33

44
import { mergeRefs } from '@/ui/utils/mergeRefs';
55
import type { FeedbackType } from '@/ui/utils/useFormControl';
@@ -9,7 +9,7 @@ import { Check, ChevronUpDown } from '../../icons';
99
import { common, type PropsOfComponent } from '../../styledSystem';
1010
import { Select, SelectButton, SelectOptionList } from '../Select';
1111
import type { CountryEntry, CountryIso } from './countryCodeData';
12-
import { IsoToCountryMap } from './countryCodeData';
12+
import { getIsoToCountryMap, isCountryCodeDataLoaded, loadCountryCodeData } from './countryCodeDataLoader';
1313
import { useFormattedPhoneNumber } from './useFormattedPhoneNumber';
1414

1515
const createSelectOption = (country: CountryEntry) => {
@@ -21,7 +21,17 @@ const createSelectOption = (country: CountryEntry) => {
2121
};
2222
};
2323

24-
const countryOptions = [...IsoToCountryMap.values()].map(createSelectOption);
24+
function useCountryCodeData() {
25+
const [loaded, setLoaded] = useState(isCountryCodeDataLoaded);
26+
27+
useEffect(() => {
28+
if (!loaded) {
29+
void loadCountryCodeData().then(() => setLoaded(true));
30+
}
31+
}, [loaded]);
32+
33+
return loaded;
34+
}
2535

2636
type PhoneInputProps = PropsOfComponent<typeof Input> & { locationBasedCountryIso?: CountryIso };
2737

@@ -34,6 +44,11 @@ const PhoneInputBase = forwardRef<HTMLInputElement, PhoneInputProps & { feedback
3444
locationBasedCountryIso,
3545
});
3646

47+
const countryOptions = useMemo(() => {
48+
const map = getIsoToCountryMap();
49+
return map ? [...map.values()].map(createSelectOption) : [];
50+
}, []);
51+
3752
const callOnChangeProp = () => {
3853
// Quick and dirty way to match this component's public API
3954
// with every other Input component, so we can use the same helpers
@@ -43,7 +58,7 @@ const PhoneInputBase = forwardRef<HTMLInputElement, PhoneInputProps & { feedback
4358

4459
const selectedCountryOption = useMemo(() => {
4560
return countryOptions.find(o => o.country.iso === iso) || countryOptions[0];
46-
}, [iso]);
61+
}, [countryOptions, iso]);
4762

4863
useEffect(callOnChangeProp, [numberWithCode]);
4964

@@ -248,6 +263,11 @@ const CountryCodeListItem = memo((props: CountryCodeListItemProps) => {
248263
export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps & { feedbackType?: FeedbackType }>(
249264
(props, ref) => {
250265
const { __internal_country } = useClerk();
266+
const dataLoaded = useCountryCodeData();
267+
268+
if (!dataLoaded) {
269+
return null;
270+
}
251271

252272
return (
253273
<PhoneInputBase

0 commit comments

Comments
 (0)