Skip to content

Commit cb32dde

Browse files
authored
fix(inputs): do not generate new field id on each render (#999)
* fix(inputs): do not generate new field id on each render
1 parent d0d087f commit cb32dde

File tree

9 files changed

+77
-20
lines changed

9 files changed

+77
-20
lines changed

src/components/fields/password-field/password-field.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
33
import { useIntl } from 'react-intl';
44
import requiredIf from 'react-required-if';
55
import useToggleState from '../../../hooks/use-toggle-state';
6+
import useFieldId from '../../../hooks/use-field-id';
67
import Constraints from '../../constraints';
78
import Spacings from '../../spacings';
89
import FieldLabel from '../../field-label';
910
import PasswordInput from '../../inputs/password-input';
1011
import FlatButton from '../../buttons/flat-button';
1112
import { EyeIcon, EyeCrossedIcon } from '../../icons';
12-
import getFieldId from '../../../utils/get-field-id';
1313
import createSequentialId from '../../../utils/create-sequential-id';
1414
import FieldErrors from '../../field-errors';
1515
import filterDataAttributes from '../../../utils/filter-data-attributes';
@@ -22,7 +22,7 @@ const hasErrors = errors => errors && Object.values(errors).some(Boolean);
2222
const PasswordField = props => {
2323
const intl = useIntl();
2424
const [isPasswordVisible, togglePasswordVisibility] = useToggleState(false);
25-
const id = getFieldId(props, {}, sequentialId);
25+
const id = useFieldId(props.id, sequentialId);
2626
const hasError = props.touched && hasErrors(props.errors);
2727

2828
return (

src/components/inputs/localized-money-input/localized-money-input.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getId,
1414
getName,
1515
} from '../../../utils/localized';
16-
import getFieldId from '../../../utils/get-field-id';
16+
import useFieldId from '../../../hooks/use-field-id';
1717
import createSequentialId from '../../../utils/create-sequential-id';
1818
import filterDataAttributes from '../../../utils/filter-data-attributes';
1919
import MoneyInput from '../money-input';
@@ -131,7 +131,7 @@ const LocalizedMoneyInput = props => {
131131
defaultExpansionState
132132
);
133133

134-
const id = getFieldId(props, {}, sequentialId);
134+
const id = useFieldId(props.id, sequentialId);
135135

136136
const hasErrorInRemainingCurrencies =
137137
props.hasError ||

src/components/inputs/localized-money-input/localized-money-input.story.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ storiesOf('Components|Inputs', module)
4545
key={key}
4646
id={text('id', undefined)}
4747
name={text('name', 'productName')}
48-
value={object('value', value)}
48+
value={value}
4949
onChange={event => {
5050
action('onChange')(event);
5151
onChange({

src/components/inputs/localized-text-input/localized-text-input.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { oneLine } from 'common-tags';
55
import { FormattedMessage } from 'react-intl';
66
import { css } from '@emotion/core';
77
import useToggleState from '../../../hooks/use-toggle-state';
8+
import useFieldId from '../../../hooks/use-field-id';
89
import ErrorMessage from '../../messages/error-message';
910
import filterDataAttributes from '../../../utils/filter-data-attributes';
1011
import Spacings from '../../spacings';
@@ -21,7 +22,6 @@ import {
2122
getId,
2223
getName,
2324
} from '../../../utils/localized';
24-
import getFieldId from '../../../utils/get-field-id';
2525
import createSequentialId from '../../../utils/create-sequential-id';
2626
import LanguagesButton from './languages-button';
2727
import messages from './messages';
@@ -136,7 +136,7 @@ const LocalizedTextInput = props => {
136136
Object.keys(props.value)
137137
);
138138

139-
const id = getFieldId(props, {}, sequentialId);
139+
const id = useFieldId(props.id, sequentialId);
140140

141141
const hasErrorInRemainingLanguages =
142142
props.hasError ||

src/components/inputs/time-input/time-input.js

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { useIntl } from 'react-intl';
4+
import useFieldId from '../../../hooks/use-field-id';
5+
import usePrevious from '../../../hooks/use-previous';
46
import filterDataAttributes from '../../../utils/filter-data-attributes';
5-
import getFieldId from '../../../utils/get-field-id';
67
import createSequentialId from '../../../utils/create-sequential-id';
78
import Constraints from '../../constraints';
89
import { parseTime } from '../../../utils/parse-time';
@@ -24,8 +25,10 @@ const format24hr = ({ hours, minutes, seconds, milliseconds }) => {
2425
const hasMilliseconds = parsedTime => parsedTime.milliseconds !== 0;
2526

2627
const TimeInput = props => {
27-
const id = getFieldId(props, {}, sequentialId);
28+
const id = useFieldId(props.id, sequentialId);
2829
const intl = useIntl();
30+
const prevLocale = usePrevious(intl.locale);
31+
2932
const { name, value, onBlur, onChange } = props;
3033

3134
const emitChange = React.useCallback(
@@ -53,15 +56,10 @@ const TimeInput = props => {
5356

5457
const onClear = React.useCallback(() => emitChange(''), [emitChange]);
5558

56-
// so that it doesn't run on initial mount
57-
const mounted = React.useRef();
58-
React.useEffect(() => {
59-
if (!mounted.current) {
60-
mounted.current = true;
61-
} else {
62-
emitChange(TimeInput.toLocaleTime(value, intl.locale));
63-
}
64-
}, [intl.locale, emitChange, value]);
59+
// if locale has changed
60+
if (typeof prevLocale !== 'undefined' && prevLocale !== intl.locale) {
61+
emitChange(TimeInput.toLocaleTime(value, intl.locale));
62+
}
6563

6664
return (
6765
<Constraints.Horizontal constraint={props.horizontalConstraint}>

src/components/tooltip/tooltip.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import styled from '@emotion/styled';
66
import isNil from 'lodash/isNil';
77
import usePopper from 'use-popper';
88
import useToggleState from '../../hooks/use-toggle-state';
9-
import getFieldId from '../../utils/get-field-id';
9+
import useFieldId from '../../hooks/use-field-id';
1010
import createSequentialId from '../../utils/create-sequential-id';
1111
import { Body, getBodyStyles } from './tooltip.styles';
1212

@@ -47,7 +47,7 @@ const Tooltip = props => {
4747

4848
const isControlled = !isNil(props.isOpen);
4949
const tooltipIsOpen = isControlled ? props.isOpen : isOpen;
50-
const id = getFieldId(props, {}, sequentialId);
50+
const id = useFieldId(props.id, sequentialId);
5151

5252
const { onClose } = props;
5353
const handleClose = React.useCallback(
@@ -188,6 +188,7 @@ Tooltip.propTypes = {
188188
}).isRequired,
189189
off: PropTypes.bool.isRequired,
190190
isOpen: PropTypes.bool,
191+
id: PropTypes.string,
191192
onClose: PropTypes.func,
192193
onOpen: PropTypes.func,
193194
placement: PropTypes.oneOf([

src/hooks/use-field-id/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './use-field-id';
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import getFieldId from '../../utils/get-field-id';
3+
4+
const useFieldId = (id, sequentialId) => {
5+
const [internalId, setId] = React.useState(id);
6+
7+
React.useEffect(() => {
8+
setId(getFieldId({ id }, { id: internalId }, sequentialId));
9+
}, [id, internalId, setId, sequentialId]);
10+
11+
return internalId;
12+
};
13+
14+
export default useFieldId;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react';
2+
import { render } from '../../test-utils';
3+
import createSequentialId from '../../utils/create-sequential-id';
4+
import useToggleState from '../use-toggle-state';
5+
import useFieldId from './use-field-id';
6+
7+
const sequentialId = createSequentialId('test-id-');
8+
9+
const TestComponent = props => {
10+
// eslint-disable-next-line react/prop-types
11+
const [isToggled, toggle] = useToggleState(false);
12+
// eslint-disable-next-line react/prop-types
13+
const id = useFieldId(props.id, sequentialId);
14+
15+
return (
16+
<div id={id}>
17+
<button data-testid="toggle-btn" onClick={toggle}>
18+
Toggle
19+
</button>
20+
<div data-testid="test-ctn">{isToggled ? 'Foo' : 'Bar'}</div>
21+
</div>
22+
);
23+
};
24+
25+
describe('when id not provided', () => {
26+
it('should use sequential-id and not increment on rerender', () => {
27+
const { container, getByTestId } = render(<TestComponent />);
28+
expect(container.querySelector('#test-id-1')).toBeInTheDocument();
29+
getByTestId('toggle-btn').click();
30+
expect(container.querySelector('#test-id-1')).toBeInTheDocument();
31+
getByTestId('toggle-btn').click();
32+
expect(container.querySelector('#test-id-1')).toBeInTheDocument();
33+
});
34+
});
35+
36+
describe('when id is provided', () => {
37+
it('should use provided id and not change on rerender', () => {
38+
const { container, getByTestId } = render(<TestComponent id="foo-bar" />);
39+
expect(container.querySelector('#foo-bar')).toBeInTheDocument();
40+
getByTestId('toggle-btn').click();
41+
expect(container.querySelector('#foo-bar')).toBeInTheDocument();
42+
});
43+
});

0 commit comments

Comments
 (0)