Skip to content

Commit f4c87e9

Browse files
authored
refactor: fix performance of InputLabel (#3662)
1 parent c0cd6c9 commit f4c87e9

File tree

6 files changed

+152
-97
lines changed

6 files changed

+152
-97
lines changed

src/components/TextInput/Label/InputLabel.tsx

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import AnimatedText from '../../Typography/AnimatedText';
55
import type { InputLabelProps } from '../types';
66

77
const InputLabel = (props: InputLabelProps) => {
8-
const { parentState, labelBackground } = props;
98
const {
10-
label,
9+
labeled,
10+
wiggle,
1111
error,
12+
focused,
13+
opacity,
14+
labelLayoutWidth,
15+
labelBackground,
16+
label,
17+
labelError,
1218
onLayoutAnimatedText,
1319
hasActiveOutline,
1420
activeColor,
@@ -23,20 +29,26 @@ const InputLabel = (props: InputLabelProps) => {
2329
wiggleOffsetX,
2430
labelScale,
2531
topPosition,
26-
paddingOffset,
32+
paddingLeft,
33+
paddingRight,
34+
backgroundColor,
35+
roundness,
2736
placeholderColor,
2837
errorColor,
2938
labelTranslationXOffset,
3039
maxFontSizeMultiplier,
3140
testID,
3241
theme,
33-
} = props.labelProps;
42+
} = props;
43+
44+
const paddingOffset =
45+
paddingLeft && paddingRight ? { paddingLeft, paddingRight } : {};
3446

3547
const labelTranslationX = {
3648
transform: [
3749
{
3850
// Offset label scale since RN doesn't support transform origin
39-
translateX: parentState.labeled.interpolate({
51+
translateX: labeled.interpolate({
4052
inputRange: [0, 1],
4153
outputRange: [baseLabelTranslateX, labelTranslationXOffset || 0],
4254
}),
@@ -50,27 +62,26 @@ const InputLabel = (props: InputLabelProps) => {
5062
lineHeight,
5163
fontWeight,
5264
opacity: hasActiveOutline
53-
? parentState.labeled.interpolate({
65+
? labeled.interpolate({
5466
inputRange: [0, 1],
5567
outputRange: [1, 0],
5668
})
5769
: 0,
5870
transform: [
5971
{
6072
// Wiggle the label when there's an error
61-
translateX:
62-
parentState.value && error
63-
? parentState.error.interpolate({
64-
inputRange: [0, 0.5, 1],
65-
outputRange: [0, wiggleOffsetX, 0],
66-
})
67-
: 0,
73+
translateX: wiggle
74+
? error.interpolate({
75+
inputRange: [0, 0.5, 1],
76+
outputRange: [0, wiggleOffsetX, 0],
77+
})
78+
: 0,
6879
},
6980
{
7081
// Move label to top
7182
translateY:
7283
baseLabelTranslateY !== 0
73-
? parentState.labeled.interpolate({
84+
? labeled.interpolate({
7485
inputRange: [0, 1],
7586
outputRange: [baseLabelTranslateY, 0],
7687
})
@@ -80,23 +91,17 @@ const InputLabel = (props: InputLabelProps) => {
8091
// Make label smaller
8192
scale:
8293
labelScale !== 0
83-
? parentState.labeled.interpolate({
94+
? labeled.interpolate({
8495
inputRange: [0, 1],
8596
outputRange: [labelScale, 1],
8697
})
87-
: parentState.labeled,
98+
: labeled,
8899
},
89100
],
90101
};
91102

92-
const textColor = error && errorColor ? errorColor : placeholderColor;
93-
// Hide the label in minimized state until we measure it's width
94-
const opacity =
95-
parentState.value || parentState.focused
96-
? parentState.labelLayout.measured
97-
? 1
98-
: 0
99-
: 1;
103+
const textColor = labelError && errorColor ? errorColor : placeholderColor;
104+
100105
return (
101106
// Position colored placeholder and gray placeholder on top of each other and crossfade them
102107
// This gives the effect of animating the color, but allows us to use native driver
@@ -110,10 +115,16 @@ const InputLabel = (props: InputLabelProps) => {
110115
]}
111116
>
112117
{labelBackground?.({
113-
parentState,
118+
labeled,
119+
labelLayoutWidth,
114120
labelStyle,
115121
theme,
116-
labelProps: props.labelProps,
122+
placeholderStyle,
123+
baseLabelTranslateX,
124+
topPosition,
125+
label,
126+
backgroundColor,
127+
roundness,
117128
maxFontSizeMultiplier: maxFontSizeMultiplier,
118129
})}
119130
<AnimatedText
@@ -137,7 +148,7 @@ const InputLabel = (props: InputLabelProps) => {
137148
{label}
138149
</AnimatedText>
139150
<AnimatedText
140-
variant={parentState.focused ? 'bodyLarge' : 'bodySmall'}
151+
variant={focused ? 'bodyLarge' : 'bodySmall'}
141152
style={[
142153
placeholderStyle,
143154
{
@@ -166,4 +177,4 @@ const styles = StyleSheet.create({
166177
},
167178
});
168179

169-
export default InputLabel;
180+
export default React.memo(InputLabel);

src/components/TextInput/Label/LabelBackground.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,34 @@ import AnimatedText from '../../Typography/AnimatedText';
66
import type { LabelBackgroundProps } from '../types';
77

88
const LabelBackground = ({
9-
parentState,
10-
labelProps: {
11-
placeholderStyle,
12-
baseLabelTranslateX,
13-
topPosition,
14-
label,
15-
backgroundColor,
16-
roundness,
17-
},
9+
labeled,
10+
labelLayoutWidth,
11+
placeholderStyle,
12+
baseLabelTranslateX,
13+
topPosition,
14+
label,
15+
backgroundColor,
16+
roundness,
1817
labelStyle,
1918
maxFontSizeMultiplier,
2019
theme: themeOverrides,
2120
}: LabelBackgroundProps) => {
22-
const opacity = parentState.labeled.interpolate({
21+
const opacity = labeled.interpolate({
2322
inputRange: [0, 0.6],
2423
outputRange: [1, 0],
2524
});
2625

2726
const { isV3 } = useInternalTheme(themeOverrides);
2827

2928
const labelTranslationX = {
30-
translateX: parentState.labeled.interpolate({
29+
translateX: labeled.interpolate({
3130
inputRange: [0, 1],
3231
outputRange: [-baseLabelTranslateX, 0],
3332
}),
3433
};
3534

3635
const labelTextScaleY = {
37-
scaleY: parentState.labeled.interpolate({
36+
scaleY: labeled.interpolate({
3837
inputRange: [0, 1],
3938
outputRange: [0.2, 1],
4039
}),
@@ -44,13 +43,10 @@ const LabelBackground = ({
4443

4544
const labelTextWidth = isV3
4645
? {
47-
width:
48-
parentState.labelLayout.width - placeholderStyle.paddingHorizontal,
46+
width: labelLayoutWidth - placeholderStyle.paddingHorizontal,
4947
}
5048
: {
51-
maxWidth:
52-
parentState.labelLayout.width -
53-
2 * placeholderStyle.paddingHorizontal,
49+
maxWidth: labelLayoutWidth - 2 * placeholderStyle.paddingHorizontal,
5450
};
5551

5652
const isRounded = roundness > 6;

src/components/TextInput/TextInput.tsx

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ type TextInputHandles = Pick<
222222
* @extends TextInput props https://reactnative.dev/docs/textinput#props
223223
*/
224224

225+
const DefaultRenderer = (props: RenderProps) => <NativeTextInput {...props} />;
226+
225227
const TextInput = forwardRef<TextInputHandles, Props>(
226228
(
227229
{
@@ -232,7 +234,7 @@ const TextInput = forwardRef<TextInputHandles, Props>(
232234
multiline = false,
233235
editable = true,
234236
contentStyle,
235-
render = (props: RenderProps) => <NativeTextInput {...props} />,
237+
render = DefaultRenderer,
236238
theme: themeOverrides,
237239
...rest
238240
}: Props,
@@ -380,29 +382,35 @@ const TextInput = forwardRef<TextInputHandles, Props>(
380382
}
381383
}, [focused, value, labeled, scale]);
382384

383-
const onLeftAffixLayoutChange = (event: LayoutChangeEvent) => {
384-
const height = roundLayoutSize(event.nativeEvent.layout.height);
385-
const width = roundLayoutSize(event.nativeEvent.layout.width);
385+
const onLeftAffixLayoutChange = React.useCallback(
386+
(event: LayoutChangeEvent) => {
387+
const height = roundLayoutSize(event.nativeEvent.layout.height);
388+
const width = roundLayoutSize(event.nativeEvent.layout.width);
386389

387-
if (width !== leftLayout.width || height !== leftLayout.height) {
388-
setLeftLayout({
389-
width,
390-
height,
391-
});
392-
}
393-
};
390+
if (width !== leftLayout.width || height !== leftLayout.height) {
391+
setLeftLayout({
392+
width,
393+
height,
394+
});
395+
}
396+
},
397+
[leftLayout.height, leftLayout.width]
398+
);
394399

395-
const onRightAffixLayoutChange = (event: LayoutChangeEvent) => {
396-
const width = roundLayoutSize(event.nativeEvent.layout.width);
397-
const height = roundLayoutSize(event.nativeEvent.layout.height);
400+
const onRightAffixLayoutChange = React.useCallback(
401+
(event: LayoutChangeEvent) => {
402+
const width = roundLayoutSize(event.nativeEvent.layout.width);
403+
const height = roundLayoutSize(event.nativeEvent.layout.height);
398404

399-
if (width !== rightLayout.width || height !== rightLayout.height) {
400-
setRightLayout({
401-
width,
402-
height,
403-
});
404-
}
405-
};
405+
if (width !== rightLayout.width || height !== rightLayout.height) {
406+
setRightLayout({
407+
width,
408+
height,
409+
});
410+
}
411+
},
412+
[rightLayout.height, rightLayout.width]
413+
);
406414

407415
const handleFocus = (args: any) => {
408416
if (disabled || !editable) {
@@ -435,19 +443,23 @@ const TextInput = forwardRef<TextInputHandles, Props>(
435443
rest.onChangeText?.(value);
436444
};
437445

438-
const handleLayoutAnimatedText = (e: LayoutChangeEvent) => {
439-
const width = roundLayoutSize(e.nativeEvent.layout.width);
440-
const height = roundLayoutSize(e.nativeEvent.layout.height);
446+
const handleLayoutAnimatedText = React.useCallback(
447+
(e: LayoutChangeEvent) => {
448+
const width = roundLayoutSize(e.nativeEvent.layout.width);
449+
const height = roundLayoutSize(e.nativeEvent.layout.height);
450+
451+
if (width !== labelLayout.width || height !== labelLayout.height) {
452+
setLabelLayout({
453+
width,
454+
height,
455+
measured: true,
456+
});
457+
}
458+
},
459+
[labelLayout.height, labelLayout.width]
460+
);
441461

442-
if (width !== labelLayout.width || height !== labelLayout.height) {
443-
setLabelLayout({
444-
width,
445-
height,
446-
measured: true,
447-
});
448-
}
449-
};
450-
const forceFocus = () => root.current?.focus();
462+
const forceFocus = React.useCallback(() => root.current?.focus(), []);
451463

452464
const { maxFontSizeMultiplier = 1.5 } = rest;
453465

src/components/TextInput/TextInputFlat.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ const TextInputFlat = ({
252252
label,
253253
onLayoutAnimatedText,
254254
placeholderOpacity,
255-
error,
255+
labelError: error,
256256
placeholderStyle: styles.placeholder,
257257
baseLabelTranslateY,
258258
baseLabelTranslateX,
@@ -263,12 +263,16 @@ const TextInputFlat = ({
263263
labelScale,
264264
wiggleOffsetX: LABEL_WIGGLE_X_OFFSET,
265265
topPosition,
266-
paddingOffset: isAndroid
267-
? {
268-
paddingLeft: I18nManager.isRTL ? paddingRight : paddingLeft,
269-
paddingRight: I18nManager.isRTL ? paddingLeft : paddingRight,
270-
}
271-
: { paddingRight, paddingLeft },
266+
paddingLeft: isAndroid
267+
? I18nManager.isRTL
268+
? paddingRight
269+
: paddingLeft
270+
: paddingLeft,
271+
paddingRight: isAndroid
272+
? I18nManager.isRTL
273+
? paddingLeft
274+
: paddingRight
275+
: paddingRight,
272276
hasActiveOutline,
273277
activeColor,
274278
placeholderColor,
@@ -278,7 +282,14 @@ const TextInputFlat = ({
278282
testID,
279283
contentStyle,
280284
theme,
285+
opacity:
286+
parentState.value || parentState.focused
287+
? parentState.labelLayout.measured
288+
? 1
289+
: 0
290+
: 1,
281291
};
292+
282293
const affixTopPosition = {
283294
[AdornmentSide.Left]: leftAffixTopPosition,
284295
[AdornmentSide.Right]: rightAffixTopPosition,
@@ -349,7 +360,15 @@ const TextInputFlat = ({
349360
/>
350361
)}
351362
{label ? (
352-
<InputLabel parentState={parentState} labelProps={labelProps} />
363+
<InputLabel
364+
labeled={parentState.labeled}
365+
error={parentState.error}
366+
focused={parentState.focused}
367+
wiggle={Boolean(parentState.value && labelProps.labelError)}
368+
labelLayoutMeasured={parentState.labelLayout.measured}
369+
labelLayoutWidth={parentState.labelLayout.width}
370+
{...labelProps}
371+
/>
353372
) : null}
354373
{render?.({
355374
testID,

0 commit comments

Comments
 (0)