Skip to content

Commit 0d9addb

Browse files
committed
Add useUniqueId hook
This hook returns a stable value across multiple rerenders, and can optionally be overridden with a fixed value Use this hook in several functional components that currently have unstable ids that change between rerenders. Populate the idFactory of this hook in the AppProvider, with eye to eventually allowing consumers to override the idFactory so it can be reset between server renders (making this public and configurable is a follow up step)
1 parent df56f15 commit 0d9addb

File tree

17 files changed

+302
-69
lines changed

17 files changed

+302
-69
lines changed

src/components/AppProvider/AppProvider.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import {
1616
StickyManagerContext,
1717
} from '../../utilities/sticky-manager';
1818
import {LinkContext, LinkLikeComponent} from '../../utilities/link';
19+
import {
20+
UniqueIdFactory,
21+
UniqueIdFactoryContext,
22+
globalIdGeneratorFactory,
23+
} from '../../utilities/unique-id';
1924

2025
interface State {
2126
intl: I18n;
@@ -35,11 +40,14 @@ export interface AppProviderProps extends AppBridgeOptions {
3540
export class AppProvider extends React.Component<AppProviderProps, State> {
3641
private stickyManager: StickyManager;
3742
private scrollLockManager: ScrollLockManager;
43+
private uniqueIdFactory: UniqueIdFactory;
3844

3945
constructor(props: AppProviderProps) {
4046
super(props);
4147
this.stickyManager = new StickyManager();
4248
this.scrollLockManager = new ScrollLockManager();
49+
this.uniqueIdFactory = new UniqueIdFactory(globalIdGeneratorFactory);
50+
4351
const {i18n, apiKey, shopOrigin, forceRedirect, linkComponent} = this.props;
4452

4553
// eslint-disable-next-line react/state-in-constructor
@@ -91,13 +99,15 @@ export class AppProvider extends React.Component<AppProviderProps, State> {
9199
<I18nContext.Provider value={intl}>
92100
<ScrollLockManagerContext.Provider value={this.scrollLockManager}>
93101
<StickyManagerContext.Provider value={this.stickyManager}>
94-
<AppBridgeContext.Provider value={appBridge}>
95-
<LinkContext.Provider value={link}>
96-
<ThemeProvider theme={theme}>
97-
{React.Children.only(children)}
98-
</ThemeProvider>
99-
</LinkContext.Provider>
100-
</AppBridgeContext.Provider>
102+
<UniqueIdFactoryContext.Provider value={this.uniqueIdFactory}>
103+
<AppBridgeContext.Provider value={appBridge}>
104+
<LinkContext.Provider value={link}>
105+
<ThemeProvider theme={theme}>
106+
{React.Children.only(children)}
107+
</ThemeProvider>
108+
</LinkContext.Provider>
109+
</AppBridgeContext.Provider>
110+
</UniqueIdFactoryContext.Provider>
101111
</StickyManagerContext.Provider>
102112
</ScrollLockManagerContext.Provider>
103113
</I18nContext.Provider>

src/components/ChoiceList/ChoiceList.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
2-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
32

43
import {classNames} from '../../utilities/css';
4+
import {useUniqueId} from '../../utilities/unique-id';
55
import {Checkbox} from '../Checkbox';
66
import {RadioButton} from '../RadioButton';
77
import {InlineError, errorTextID} from '../InlineError';
@@ -48,8 +48,6 @@ export interface ChoiceListProps {
4848
onChange?(selected: string[], name: string): void;
4949
}
5050

51-
const getUniqueID = createUniqueIDFactory('ChoiceList');
52-
5351
export function ChoiceList({
5452
title,
5553
titleHidden,
@@ -59,12 +57,13 @@ export function ChoiceList({
5957
onChange = noop,
6058
error,
6159
disabled = false,
62-
name = getUniqueID(),
60+
name: nameProp,
6361
}: ChoiceListProps) {
6462
// Type asserting to any is required for TS3.2 but can be removed when we update to 3.3
6563
// see https://github.com/Microsoft/TypeScript/issues/28768
6664
const ControlComponent: any = allowMultiple ? Checkbox : RadioButton;
6765

66+
const name = useUniqueId('ChoiceList', nameProp);
6867
const finalName = allowMultiple ? `${name}[]` : name;
6968

7069
const className = classNames(

src/components/FormLayout/components/Group/Group.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react';
2-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
32

43
import {classNames} from '../../../../utilities/css';
54
import {wrapWithComponent} from '../../../../utilities/components';
5+
import {useUniqueId} from '../../../../utilities/unique-id';
66
import styles from '../../FormLayout.scss';
77
import {Item} from '../Item';
88

@@ -13,12 +13,10 @@ export interface GroupProps {
1313
helpText?: React.ReactNode;
1414
}
1515

16-
const getUniqueID = createUniqueIDFactory('FormLayoutGroup');
17-
1816
export function Group({children, condensed, title, helpText}: GroupProps) {
1917
const className = classNames(condensed ? styles.condensed : styles.grouped);
2018

21-
const id = getUniqueID();
19+
const id = useUniqueId('FormLayoutGroup');
2220

2321
let helpTextElement = null;
2422
let helpTextID: undefined | string;

src/components/Navigation/components/Item/components/Secondary/Secondary.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import React from 'react';
2-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
32

3+
import {useUniqueId} from '../../../../../../utilities/unique-id';
44
import {Collapsible} from '../../../../../Collapsible';
55

66
import styles from '../../../../Navigation.scss';
77

8-
const createSecondaryNavigationId = createUniqueIDFactory(
9-
'SecondaryNavigation',
10-
);
11-
128
interface SecondaryProps {
139
expanded: boolean;
1410
children?: React.ReactNode;
1511
}
1612

1713
export function Secondary({children, expanded}: SecondaryProps) {
18-
const secondaryNavigationId = createSecondaryNavigationId();
14+
const id = useUniqueId('SecondaryNavigation');
1915
return (
20-
<Collapsible id={secondaryNavigationId} open={expanded}>
16+
<Collapsible id={id} open={expanded}>
2117
<ul className={styles.List}>{children}</ul>
2218
</Collapsible>
2319
);

src/components/OptionList/OptionList.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React, {useState, useRef, useCallback} from 'react';
2-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
1+
import React, {useState, useCallback} from 'react';
32

43
import {arraysAreEqual} from '../../utilities/arrays';
54
import {IconProps} from '../../types';
65
import {AvatarProps} from '../Avatar';
76
import {ThumbnailProps} from '../Thumbnail';
7+
import {useUniqueId} from '../../utilities/unique-id';
88
import {useDeepEffect} from '../../utilities/use-deep-effect';
99

1010
import {Option} from './components';
@@ -34,8 +34,6 @@ export interface SectionDescriptor {
3434

3535
type Descriptor = OptionDescriptor | SectionDescriptor;
3636

37-
const getUniqueId = createUniqueIDFactory('OptionList');
38-
3937
export interface OptionListProps {
4038
/** A unique identifier for the option list */
4139
id?: string;
@@ -66,16 +64,12 @@ export function OptionList({
6664
role,
6765
optionRole,
6866
onChange,
69-
id: propId,
67+
id: idProp,
7068
}: OptionListProps) {
7169
const [normalizedOptions, setNormalizedOptions] = useState(
7270
createNormalizedOptions(options, sections, title),
7371
);
74-
const id = useRef(propId || getUniqueId());
75-
76-
if (id.current !== propId) {
77-
id.current = propId || id.current;
78-
}
72+
const id = useUniqueId('OptionList', idProp);
7973

8074
useDeepEffect(
8175
() => {
@@ -122,7 +116,7 @@ export function OptionList({
122116
options.map((option, optionIndex) => {
123117
const isSelected = selected.includes(option.value);
124118
const optionId =
125-
option.id || `${id.current}-${sectionIndex}-${optionIndex}`;
119+
option.id || `${id}-${sectionIndex}-${optionIndex}`;
126120

127121
return (
128122
<Option
@@ -144,7 +138,7 @@ export function OptionList({
144138
{titleMarkup}
145139
<ul
146140
className={styles.Options}
147-
id={`${id.current}-${sectionIndex}`}
141+
id={`${id}-${sectionIndex}`}
148142
role={role}
149143
aria-multiselectable={allowMultiple}
150144
>

src/components/OptionList/components/Checkbox/Checkbox.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import {TickSmallMinor} from '@shopify/polaris-icons';
3-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
43
import {classNames} from '../../../../utilities/css';
4+
import {useUniqueId} from '../../../../utilities/unique-id';
55
import {Icon} from '../../../Icon';
66

77
import styles from './Checkbox.scss';
@@ -17,10 +17,8 @@ export interface CheckboxProps {
1717
onChange(): void;
1818
}
1919

20-
const getUniqueID = createUniqueIDFactory('Checkbox');
21-
2220
export function Checkbox({
23-
id = getUniqueID(),
21+
id: idProp,
2422
checked = false,
2523
disabled,
2624
active,
@@ -29,6 +27,8 @@ export function Checkbox({
2927
value,
3028
role,
3129
}: CheckboxProps) {
30+
const id = useUniqueId('Checkbox', idProp);
31+
3232
const className = classNames(styles.Checkbox, active && styles.active);
3333
return (
3434
<div className={className}>

src/components/PolarisTestProvider/PolarisTestProvider.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
import {AppBridgeContext, AppBridgeOptions} from '../../utilities/app-bridge';
1313
import {I18n, I18nContext, TranslationDictionary} from '../../utilities/i18n';
1414
import {LinkContext, LinkLikeComponent} from '../../utilities/link';
15+
import {
16+
UniqueIdFactory,
17+
UniqueIdFactoryContext,
18+
globalIdGeneratorFactory,
19+
} from '../../utilities/unique-id';
1520

1621
type FrameContextType = NonNullable<React.ContextType<typeof FrameContext>>;
1722

@@ -53,6 +58,8 @@ export function PolarisTestProvider({
5358

5459
const stickyManager = new StickyManager();
5560

61+
const uniqueIdFactory = new UniqueIdFactory(globalIdGeneratorFactory);
62+
5663
// This typing is odd, but as appBridge is deprecated and going away in v5
5764
// I'm not that worried about it
5865
const appBridgeApp = appBridge as React.ContextType<typeof AppBridgeContext>;
@@ -66,15 +73,17 @@ export function PolarisTestProvider({
6673
<I18nContext.Provider value={intl}>
6774
<ScrollLockManagerContext.Provider value={scrollLockManager}>
6875
<StickyManagerContext.Provider value={stickyManager}>
69-
<AppBridgeContext.Provider value={appBridgeApp}>
70-
<LinkContext.Provider value={link}>
71-
<ThemeContext.Provider value={mergedTheme}>
72-
<FrameContext.Provider value={mergedFrame}>
73-
{children}
74-
</FrameContext.Provider>
75-
</ThemeContext.Provider>
76-
</LinkContext.Provider>
77-
</AppBridgeContext.Provider>
76+
<UniqueIdFactoryContext.Provider value={uniqueIdFactory}>
77+
<AppBridgeContext.Provider value={appBridgeApp}>
78+
<LinkContext.Provider value={link}>
79+
<ThemeContext.Provider value={mergedTheme}>
80+
<FrameContext.Provider value={mergedFrame}>
81+
{children}
82+
</FrameContext.Provider>
83+
</ThemeContext.Provider>
84+
</LinkContext.Provider>
85+
</AppBridgeContext.Provider>
86+
</UniqueIdFactoryContext.Provider>
7887
</StickyManagerContext.Provider>
7988
</ScrollLockManagerContext.Provider>
8089
</I18nContext.Provider>

src/components/RadioButton/RadioButton.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
2+
import {useUniqueId} from '../../utilities/unique-id';
33
import {Choice, helpTextID} from '../Choice';
44
import styles from './RadioButton.scss';
55

@@ -32,8 +32,6 @@ export interface BaseProps {
3232

3333
export interface RadioButtonProps extends BaseProps {}
3434

35-
const getUniqueID = createUniqueIDFactory('RadioButton');
36-
3735
export function RadioButton({
3836
ariaDescribedBy: ariaDescribedByProp,
3937
label,
@@ -44,10 +42,13 @@ export function RadioButton({
4442
onChange,
4543
onFocus,
4644
onBlur,
47-
id = getUniqueID(),
48-
name = id,
45+
id: providedId,
46+
name: providedName,
4947
value,
5048
}: RadioButtonProps) {
49+
const id = useUniqueId('RadioButton', providedId);
50+
const name = providedName || id;
51+
5152
function handleChange({currentTarget}: React.ChangeEvent<HTMLInputElement>) {
5253
onChange && onChange(currentTarget.checked, id);
5354
}

src/components/Scrollable/components/ScrollTo/ScrollTo.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {useContext, useEffect, useRef} from 'react';
2-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
2+
import {useUniqueId} from '../../../../utilities/unique-id';
33
import {ScrollableContext} from '../../context';
44

55
export function ScrollTo() {
@@ -14,7 +14,7 @@ export function ScrollTo() {
1414
scrollToPosition(anchorNode.current.offsetTop);
1515
}, [scrollToPosition]);
1616

17-
const getUniqueId = createUniqueIDFactory(`ScrollTo`);
17+
const id = useUniqueId(`ScrollTo`);
1818
// eslint-disable-next-line jsx-a11y/anchor-is-valid
19-
return <a id={getUniqueId()} ref={anchorNode} />;
19+
return <a id={id} ref={anchorNode} />;
2020
}

src/components/Select/Select.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React from 'react';
22
import {ArrowUpDownMinor} from '@shopify/polaris-icons';
3-
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
4-
53
import {classNames} from '../../utilities/css';
4+
import {useUniqueId} from '../../utilities/unique-id';
65
import {Labelled, Action, helpTextID} from '../Labelled';
76
import {Icon} from '../Icon';
87
import {Error} from '../../types';
@@ -72,7 +71,6 @@ export interface BaseProps {
7271
export interface SelectProps extends BaseProps {}
7372

7473
const PLACEHOLDER_VALUE = '';
75-
const getUniqueID = createUniqueIDFactory('Select');
7674

7775
export function Select({
7876
options: optionsProp,
@@ -83,14 +81,16 @@ export function Select({
8381
disabled,
8482
helpText,
8583
placeholder,
86-
id = getUniqueID(),
84+
id: idProp,
8785
name,
8886
value = PLACEHOLDER_VALUE,
8987
error,
9088
onChange,
9189
onFocus,
9290
onBlur,
9391
}: SelectProps) {
92+
const id = useUniqueId('Select', idProp);
93+
9494
const labelHidden = labelInline ? true : labelHiddenProp;
9595

9696
const className = classNames(

0 commit comments

Comments
 (0)