Extract TextInput synchronization mechanism to a custom hook (#45315)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/45315

Changelog: [internal]

This moves some logic from the `TextInput` component to its own hook. It's just a refactor in preparation for a following change were we're going to test replacing this hook with an alternative version that relies less on state (using refs for some things instead).

Reviewed By: sammy-SC

Differential Revision: D59400614

fbshipit-source-id: ea37b8514f89e94be1386774ad70d56389878886
This commit is contained in:
Rubén Norte
2024-07-08 09:42:46 -07:00
committed by Facebook GitHub Bot
parent ca17d87ed1
commit 1265958d52
@@ -957,8 +957,100 @@ export type Props = $ReadOnly<{|
value?: ?Stringish,
|}>;
type ViewCommands = $NonMaybeType<
| typeof AndroidTextInputCommands
| typeof RCTMultilineTextInputNativeCommands
| typeof RCTSinglelineTextInputNativeCommands,
>;
type LastNativeSelection = {|
selection: Selection,
mostRecentEventCount: number,
|};
const emptyFunctionThatReturnsTrue = () => true;
/**
* This hook handles the synchronization between the state of the text input
* in native and in JavaScript. This is necessary due to the asynchronous nature
* of text input events.
*/
function useTextInputStateSynchronization({
props,
mostRecentEventCount,
selection,
inputRef,
text,
viewCommands,
}: {
props: Props,
mostRecentEventCount: number,
selection: ?Selection,
inputRef: React.RefObject<null | React.ElementRef<HostComponent<mixed>>>,
text: string,
viewCommands: ViewCommands,
}): {
setLastNativeText: string => void,
setLastNativeSelection: LastNativeSelection => void,
} {
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
const [lastNativeSelectionState, setLastNativeSelection] =
useState<LastNativeSelection>({
selection: {start: -1, end: -1},
mostRecentEventCount: mostRecentEventCount,
});
const lastNativeSelection = lastNativeSelectionState.selection;
// This is necessary in case native updates the text and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
useLayoutEffect(() => {
const nativeUpdate: {text?: string, selection?: Selection} = {};
if (lastNativeText !== props.value && typeof props.value === 'string') {
nativeUpdate.text = props.value;
setLastNativeText(props.value);
}
if (
selection &&
lastNativeSelection &&
(lastNativeSelection.start !== selection.start ||
lastNativeSelection.end !== selection.end)
) {
nativeUpdate.selection = selection;
setLastNativeSelection({selection, mostRecentEventCount});
}
if (Object.keys(nativeUpdate).length === 0) {
return;
}
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
text,
selection?.start ?? -1,
selection?.end ?? -1,
);
}
}, [
mostRecentEventCount,
inputRef,
props.value,
props.defaultValue,
lastNativeText,
selection,
lastNativeSelection,
text,
viewCommands,
]);
return {setLastNativeText, setLastNativeSelection};
}
/**
* A foundational component for inputting text into the app via a
* keyboard. Props provide configurability for several features, such as
@@ -1098,28 +1190,6 @@ function InternalTextInput(props: Props): React.Node {
end: propsSelection.end ?? propsSelection.start,
};
const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
const [lastNativeSelectionState, setLastNativeSelection] = useState<{|
selection: Selection,
mostRecentEventCount: number,
|}>({
selection: {start: -1, end: -1},
mostRecentEventCount: mostRecentEventCount,
});
const lastNativeSelection = lastNativeSelectionState.selection;
let viewCommands;
if (AndroidTextInputCommands) {
viewCommands = AndroidTextInputCommands;
} else {
viewCommands =
props.multiline === true
? RCTMultilineTextInputNativeCommands
: RCTSinglelineTextInputNativeCommands;
}
const text =
typeof props.value === 'string'
? props.value
@@ -1127,51 +1197,22 @@ function InternalTextInput(props: Props): React.Node {
? props.defaultValue
: '';
// This is necessary in case native updates the text and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
useLayoutEffect(() => {
const nativeUpdate: {text?: string, selection?: Selection} = {};
const viewCommands =
AndroidTextInputCommands ||
(props.multiline === true
? RCTMultilineTextInputNativeCommands
: RCTSinglelineTextInputNativeCommands);
if (lastNativeText !== props.value && typeof props.value === 'string') {
nativeUpdate.text = props.value;
setLastNativeText(props.value);
}
if (
selection &&
lastNativeSelection &&
(lastNativeSelection.start !== selection.start ||
lastNativeSelection.end !== selection.end)
) {
nativeUpdate.selection = selection;
setLastNativeSelection({selection, mostRecentEventCount});
}
if (Object.keys(nativeUpdate).length === 0) {
return;
}
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
text,
selection?.start ?? -1,
selection?.end ?? -1,
);
}
}, [
mostRecentEventCount,
inputRef,
props.value,
props.defaultValue,
lastNativeText,
selection,
lastNativeSelection,
text,
viewCommands,
]);
const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
const {setLastNativeText, setLastNativeSelection} =
useTextInputStateSynchronization({
props,
inputRef,
mostRecentEventCount,
selection,
text,
viewCommands,
});
useLayoutEffect(() => {
const inputRefValue = inputRef.current;
@@ -1187,7 +1228,7 @@ function InternalTextInput(props: Props): React.Node {
}
};
}
}, [inputRef]);
}, []);
const setLocalRef = useCallback(
(instance: TextInputInstance | null) => {