Skip to content

Commit 802b43a

Browse files
rubennortefacebook-github-bot
authored andcommitted
Ship refactor for TextInput state synchronization (#49989)
Summary: Changelog: [internal] We verified that the refactor is safe using a production experiment, so we can ship this version that doesn't require state updates and has slightly better performance Reviewed By: sammy-SC Differential Revision: D71050517
1 parent 0f2a53d commit 802b43a

File tree

5 files changed

+112
-117
lines changed

5 files changed

+112
-117
lines changed

packages/react-native/Libraries/Components/TextInput/TextInput.js

+1-87
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type {
1818
import type {ViewProps} from '../View/ViewPropTypes';
1919
import type {TextInputInstance, TextInputType} from './TextInput.flow';
2020

21-
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
2221
import usePressability from '../../Pressability/usePressability';
2322
import flattenStyle from '../../StyleSheet/flattenStyle';
2423
import StyleSheet, {
@@ -1097,88 +1096,7 @@ const emptyFunctionThatReturnsTrue = () => true;
10971096
* in native and in JavaScript. This is necessary due to the asynchronous nature
10981097
* of text input events.
10991098
*/
1100-
function useTextInputStateSynchronization_STATE({
1101-
props,
1102-
mostRecentEventCount,
1103-
selection,
1104-
inputRef,
1105-
text,
1106-
viewCommands,
1107-
}: {
1108-
props: TextInputProps,
1109-
mostRecentEventCount: number,
1110-
selection: ?Selection,
1111-
inputRef: React.RefObject<null | TextInputInstance>,
1112-
text?: string,
1113-
viewCommands: ViewCommands,
1114-
}): {
1115-
setLastNativeText: string => void,
1116-
setLastNativeSelection: LastNativeSelection => void,
1117-
} {
1118-
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
1119-
const [lastNativeSelectionState, setLastNativeSelection] =
1120-
useState<LastNativeSelection>({
1121-
selection: {start: -1, end: -1},
1122-
mostRecentEventCount: mostRecentEventCount,
1123-
});
1124-
1125-
const lastNativeSelection = lastNativeSelectionState.selection;
1126-
1127-
// This is necessary in case native updates the text and JS decides
1128-
// that the update should be ignored and we should stick with the value
1129-
// that we have in JS.
1130-
useLayoutEffect(() => {
1131-
const nativeUpdate: {text?: string, selection?: Selection} = {};
1132-
1133-
if (lastNativeText !== props.value && typeof props.value === 'string') {
1134-
nativeUpdate.text = props.value;
1135-
setLastNativeText(props.value);
1136-
}
1137-
1138-
if (
1139-
selection &&
1140-
lastNativeSelection &&
1141-
(lastNativeSelection.start !== selection.start ||
1142-
lastNativeSelection.end !== selection.end)
1143-
) {
1144-
nativeUpdate.selection = selection;
1145-
setLastNativeSelection({selection, mostRecentEventCount});
1146-
}
1147-
1148-
if (Object.keys(nativeUpdate).length === 0) {
1149-
return;
1150-
}
1151-
1152-
if (inputRef.current != null) {
1153-
viewCommands.setTextAndSelection(
1154-
inputRef.current,
1155-
mostRecentEventCount,
1156-
text,
1157-
selection?.start ?? -1,
1158-
selection?.end ?? -1,
1159-
);
1160-
}
1161-
}, [
1162-
mostRecentEventCount,
1163-
inputRef,
1164-
props.value,
1165-
props.defaultValue,
1166-
lastNativeText,
1167-
selection,
1168-
lastNativeSelection,
1169-
text,
1170-
viewCommands,
1171-
]);
1172-
1173-
return {setLastNativeText, setLastNativeSelection};
1174-
}
1175-
1176-
/**
1177-
* This hook handles the synchronization between the state of the text input
1178-
* in native and in JavaScript. This is necessary due to the asynchronous nature
1179-
* of text input events.
1180-
*/
1181-
function useTextInputStateSynchronization_REFS({
1099+
function useTextInputStateSynchronization({
11821100
props,
11831101
mostRecentEventCount,
11841102
selection,
@@ -1413,10 +1331,6 @@ function InternalTextInput(props: TextInputProps): React.Node {
14131331
: RCTSinglelineTextInputNativeCommands);
14141332

14151333
const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
1416-
const useTextInputStateSynchronization =
1417-
ReactNativeFeatureFlags.useRefsForTextInputState()
1418-
? useTextInputStateSynchronization_REFS
1419-
: useTextInputStateSynchronization_STATE;
14201334
const {setLastNativeText, setLastNativeSelection} =
14211335
useTextInputStateSynchronization({
14221336
props,

packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js

+2-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*/
99

1010
const {create, update} = require('../../../../jest/renderer');
11-
const ReactNativeFeatureFlags = require('../../../../src/private/featureflags/ReactNativeFeatureFlags');
1211
const ReactNative = require('../../../ReactNative/RendererProxy');
1312
const {
1413
enter,
@@ -20,24 +19,15 @@ const ReactTestRenderer = require('react-test-renderer');
2019

2120
jest.unmock('../TextInput');
2221

23-
[
24-
{useRefsForTextInputState: true, useTextChildren: true},
25-
{useRefsForTextInputState: false, useTextChildren: true},
26-
{useRefsForTextInputState: true, useTextChildren: false},
27-
{useRefsForTextInputState: false, useTextChildren: false},
28-
].forEach(testCase => {
29-
const {useRefsForTextInputState, useTextChildren} = testCase;
30-
describe(`TextInput tests (useRefsForTextInputState = ${useRefsForTextInputState}) useTextChildren = ${useTextChildren}`, () => {
22+
[true, false].forEach(useTextChildren => {
23+
describe(`TextInput tests (useTextChildren = ${useTextChildren})`, () => {
3124
let input;
3225
let inputRef;
3326
let onChangeListener;
3427
let onChangeTextListener;
3528
const initialValue = 'initialValue';
3629
beforeEach(async () => {
3730
jest.resetModules();
38-
ReactNativeFeatureFlags.override({
39-
useRefsForTextInputState: () => useRefsForTextInputState,
40-
});
4131

4232
inputRef = React.createRef(null);
4333
onChangeListener = jest.fn();

packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap

+108
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,111 @@ exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = tru
215215
underlineColorAndroid="transparent"
216216
/>
217217
`;
218+
219+
exports[`TextInput tests (useTextChildren = false) should render as expected: should deep render when mocked (please verify output manually) 1`] = `
220+
<RCTSinglelineTextInputView
221+
accessible={true}
222+
allowFontScaling={true}
223+
focusable={true}
224+
forwardedRef={null}
225+
mostRecentEventCount={0}
226+
onBlur={[Function]}
227+
onChange={[Function]}
228+
onClick={[Function]}
229+
onFocus={[Function]}
230+
onResponderGrant={[Function]}
231+
onResponderMove={[Function]}
232+
onResponderRelease={[Function]}
233+
onResponderTerminate={[Function]}
234+
onResponderTerminationRequest={[Function]}
235+
onScroll={[Function]}
236+
onSelectionChange={[Function]}
237+
onSelectionChangeShouldSetResponder={[Function]}
238+
onStartShouldSetResponder={[Function]}
239+
rejectResponderTermination={true}
240+
selection={null}
241+
submitBehavior="blurAndSubmit"
242+
underlineColorAndroid="transparent"
243+
/>
244+
`;
245+
246+
exports[`TextInput tests (useTextChildren = false) should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
247+
<RCTSinglelineTextInputView
248+
accessible={true}
249+
allowFontScaling={true}
250+
focusable={true}
251+
forwardedRef={null}
252+
mostRecentEventCount={0}
253+
onBlur={[Function]}
254+
onChange={[Function]}
255+
onClick={[Function]}
256+
onFocus={[Function]}
257+
onResponderGrant={[Function]}
258+
onResponderMove={[Function]}
259+
onResponderRelease={[Function]}
260+
onResponderTerminate={[Function]}
261+
onResponderTerminationRequest={[Function]}
262+
onScroll={[Function]}
263+
onSelectionChange={[Function]}
264+
onSelectionChangeShouldSetResponder={[Function]}
265+
onStartShouldSetResponder={[Function]}
266+
rejectResponderTermination={true}
267+
selection={null}
268+
submitBehavior="blurAndSubmit"
269+
underlineColorAndroid="transparent"
270+
/>
271+
`;
272+
273+
exports[`TextInput tests (useTextChildren = true) should render as expected: should deep render when mocked (please verify output manually) 1`] = `
274+
<RCTSinglelineTextInputView
275+
accessible={true}
276+
allowFontScaling={true}
277+
focusable={true}
278+
forwardedRef={null}
279+
mostRecentEventCount={0}
280+
onBlur={[Function]}
281+
onChange={[Function]}
282+
onClick={[Function]}
283+
onFocus={[Function]}
284+
onResponderGrant={[Function]}
285+
onResponderMove={[Function]}
286+
onResponderRelease={[Function]}
287+
onResponderTerminate={[Function]}
288+
onResponderTerminationRequest={[Function]}
289+
onScroll={[Function]}
290+
onSelectionChange={[Function]}
291+
onSelectionChangeShouldSetResponder={[Function]}
292+
onStartShouldSetResponder={[Function]}
293+
rejectResponderTermination={true}
294+
selection={null}
295+
submitBehavior="blurAndSubmit"
296+
underlineColorAndroid="transparent"
297+
/>
298+
`;
299+
300+
exports[`TextInput tests (useTextChildren = true) should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
301+
<RCTSinglelineTextInputView
302+
accessible={true}
303+
allowFontScaling={true}
304+
focusable={true}
305+
forwardedRef={null}
306+
mostRecentEventCount={0}
307+
onBlur={[Function]}
308+
onChange={[Function]}
309+
onClick={[Function]}
310+
onFocus={[Function]}
311+
onResponderGrant={[Function]}
312+
onResponderMove={[Function]}
313+
onResponderRelease={[Function]}
314+
onResponderTerminate={[Function]}
315+
onResponderTerminationRequest={[Function]}
316+
onScroll={[Function]}
317+
onSelectionChange={[Function]}
318+
onSelectionChangeShouldSetResponder={[Function]}
319+
onStartShouldSetResponder={[Function]}
320+
rejectResponderTermination={true}
321+
selection={null}
322+
submitBehavior="blurAndSubmit"
323+
underlineColorAndroid="transparent"
324+
/>
325+
`;

packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js

-11
Original file line numberDiff line numberDiff line change
@@ -616,17 +616,6 @@ const definitions: FeatureFlagDefinitions = {
616616
},
617617
ossReleaseStage: 'none',
618618
},
619-
useRefsForTextInputState: {
620-
defaultValue: false,
621-
metadata: {
622-
dateAdded: '2024-07-08',
623-
description:
624-
'Enable a variant of TextInput that moves some state to refs to avoid unnecessary re-renders',
625-
expectedReleaseValue: true,
626-
purpose: 'experimentation',
627-
},
628-
ossReleaseStage: 'none',
629-
},
630619
},
631620
};
632621

packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<4f1befe0cec24eeb531d7ecca6bc451b>>
7+
* @generated SignedSource<<7c01ea0b9cc05fc80c73c097e68d6494>>
88
* @flow strict
99
*/
1010

@@ -40,7 +40,6 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
4040
shouldUseAnimatedObjectForTransform: Getter<boolean>,
4141
shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean>,
4242
shouldUseSetNativePropsInFabric: Getter<boolean>,
43-
useRefsForTextInputState: Getter<boolean>,
4443
}>;
4544

4645
export type ReactNativeFeatureFlagsJsOnlyOverrides = OverridesFor<ReactNativeFeatureFlagsJsOnly>;
@@ -156,11 +155,6 @@ export const shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean> = cre
156155
*/
157156
export const shouldUseSetNativePropsInFabric: Getter<boolean> = createJavaScriptFlagGetter('shouldUseSetNativePropsInFabric', true);
158157

159-
/**
160-
* Enable a variant of TextInput that moves some state to refs to avoid unnecessary re-renders
161-
*/
162-
export const useRefsForTextInputState: Getter<boolean> = createJavaScriptFlagGetter('useRefsForTextInputState', false);
163-
164158
/**
165159
* Common flag for testing. Do NOT modify.
166160
*/

0 commit comments

Comments
 (0)