This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

受控组件

学习如何在 React Native 中使用受控组件来控制 TextInput,随着用户输入时格式化文本,限制输入,并保持光标稳定。


每个输入都有一个当前值,比如输入框里的文本、开关的位置,或者选择器里的选择。受控组件会把值保存在你管理的状态里。输入框显示状态里保存的内容,用户做的每一次修改都会更新这个状态。这个状态就是唯一的真实来源。

🌐 Every input has a current value, such as the text in an input field, the position of a switch, or the selection in a picker. A controlled component keeps the value in state you manage. The input displays whatever the state holds and every change the user makes updates that state. That state is the single source of truth.

受控组件将设置值的 prop 与报告变化的回调配对。本页重点介绍 TextInput,这是 React Native 的文本输入组件,因为在文本输入中控制值最有用,同时也最难处理。至于 Switch、Checkbox 和 Picker,请参见 控制其他组件。

🌐 Controlled components pair a prop that sets the value with a callback that reports changes. This page focuses on TextInput, React Native's component for entering text, because a text input is where controlling the value is most useful and hardest to get right. For Switch, Checkbox, and Picker, see Control other components.

控制文本输入

🌐 Control a text input

React Native 的 TextInput 组件默认是非受控的。它会在内部保存文本,并且不需要 value 属性就能工作。传入 value 后,它就变成受控的。从那时起,React 会强制原生字段与该属性保持一致。

🌐 React Native's TextInput component is uncontrolled by default. It keeps its text internally and works without a value prop. Passing value makes it controlled. From then on, React forces the native field to match that prop.

下面的例子会将名字保存在状态中。value 显示状态,而 onChangeText 会在每次按键时更新它:

🌐 The following example keeps a name in state. value displays the state, and onChangeText updates it on every keystroke:

import { useState } from 'react'; import { TextInput } from 'react-native'; export default function NameInput() { const [name, setName] = useState(''); return <TextInput value={name} onChangeText={setName} placeholder="Name" />; }

在 React Native 中,这个更新循环是异步的:每次更新都会经过 JavaScript 线程。一次按键首先会更新原生字段,然后触发 onChangeText。你的状态更新会触发重新渲染,新值再返回到原生字段。在网页上,React 会同步地把输入和 DOM 对齐。大多数受控输入的问题都可以追溯到这个来回的过程。

🌐 In React Native, this update loop is asynchronous: each update round-trips through the JavaScript thread. A keystroke updates the native field first and then fires onChangeText. Your state update triggers a re-render, and the new value travels back to the native field. On the web, React reconciles inputs synchronously against the DOM. Most controlled input problems trace back to that round-trip.

下图显示了按键是如何在更新循环中传递的:

🌐 The following diagram shows how a keystroke travels through the update loop:

1

Keystroke

Native/UI thread
2

Field shows the raw text

Native/UI thread
3

onChangeText fires with the string

JavaScript thread
4

State update, then re-render

JavaScript thread
5

value forces the field to match

Native/UI thread
Between steps 2 and 5, the field shows text your state doesn't hold yet.
与网页上的受控输入的不同

如果你已经写过网页的受控输入,在 React Native 中会有三件事发生变化:

🌐 If you have written controlled inputs for the web, three things change in React Native:

  • 通常的变更处理器是 onChangeText,它直接接收新文本,而不是事件对象。
  • React Native 没有 preventDefault,所以你不能在按键出现之前阻止它。相反,你需要在 onChangeText 中清理文本,然后通过 value 把结果传回去。
  • 更新是异步的,所以字段会在你操作后一轮才反映你的状态。

下面的例子展示了两个平台上相同的输入:

🌐 The following example shows the same input on both platforms:

// Web (react-dom) <input value={text} onChange={event => setText(event.target.value)} /> // React Native <TextInput value={text} onChangeText={setText} />

在受控和非受控输入之间选择

🌐 Choose between controlled and uncontrolled inputs

当需要对每次按键作出反应时,受控输入就很有用:实时验证、统计字符数、只有在字段有效时才启用提交按钮,或者在用户输入时格式化内容。

🌐 A controlled input is useful when something has to react to every keystroke: validating live, counting characters, enabling the submit button only after the field is valid, or formatting the input as the user types.

当你只需要在交互结束时获取值时(比如用户提交搜索时),不受控输入非常有用。它还可以避免每次按键都重新渲染。要创建一个不受控输入,可以用 defaultValue 属性设置初始文本,然后用 onSubmitEditing 属性读取最终值:

🌐 An uncontrolled input is useful when you only need the value at the end of an interaction, such as when the user submits a search. It also avoids a re-render on every keystroke. To build one, set the initial text with the defaultValue prop and read the final value with the onSubmitEditing prop:

import { TextInput } from 'react-native'; export default function SearchInput({ onSearch }: { onSearch: (query: string) => void }) { return ( <TextInput defaultValue="" onSubmitEditing={event => onSearch(event.nativeEvent.text)} placeholder="Search" returnKeyType="search" /> ); }

下表比较了受控输入和不受控输入:

🌐 The following table compares controlled and uncontrolled inputs:

受控非受控
数据存储在React 状态原生输入
设置方式value 和 onChangeTextdefaultValue
读取方式React 状态onChangeText、onSubmitEditing、onEndEditing
每次输入是否重新渲染是否
适用场景实时校验、格式化、依赖 UI 的场景搜索框、提交时读取的表单

用户输入时自动格式化

🌐 Format input as users type

为了在用户输入时格式化文本,比如电话号码、货币金额或日期,把 onChangeText 里的字符串转换一下,然后把格式化后的结果存回状态里。

🌐 To format text such as a phone number, a currency amount, or a date as the user types, transform the string inside onChangeText and store the formatted result back in state.

下面的例子会在用户输入时格式化电话号码:

🌐 The following example formats a phone number as the user types:

import { useState } from 'react'; import { TextInput } from 'react-native'; function formatPhone(input: string) { const digits = input.replace(/\D/g, '').slice(0, 10); if (digits.length <= 3) return digits; if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`; return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } export default function PhoneInput() { const [phone, setPhone] = useState(''); return ( <TextInput value={phone} onChangeText={text => setPhone(formatPhone(text))} keyboardType="phone-pad" placeholder="(555) 123-4567" /> ); }

因为格式化的字符串与用户输入的内容不同,在上图步骤2到步骤5之间的间隙中,该字段会短暂显示原始文本。

🌐 Because the formatted string differs from what the user typed, the field briefly shows the raw text during the gap between steps 2 and 5 in the diagram above.

限制用户可以输入的内容

🌐 Restrict what users can type

格式化会重写整个字符串。限制输入则只是去掉字符,但使用的线路是一样的。让 onChangeText 触发,去掉不需要的字符,然后把干净的值再通过 value 推回去。

🌐 Formatting rewrites the whole string. Restricting input removes characters instead, with the same wiring. Let onChangeText fire, strip the unwanted characters, and push the clean value back through value.

下面的例子只保留字段中的数字:

🌐 The following example keeps only digits in the field:

<TextInput value={amount} onChangeText={text => setAmount(text.replace(/[^0-9]/g, ''))} keyboardType="number-pad" />

对于长度限制和只读字段,推荐使用 maxLength 和 editable 属性。它们在原生端生效,而且不会闪烁:

🌐 For length limits and read-only fields, prefer the maxLength and editable props. They apply on the native side and do not flicker:

// Cap the length <TextInput value={code} onChangeText={setCode} maxLength={6} /> // Prevent all edits <TextInput value={value} editable={false} />

设置 keyboardType(例如,number-pad、decimal-pad 或 phone-pad)会显示匹配的键盘,但它不会强制执行任何操作:硬件键盘和粘贴的文本仍然可以输入其他字符,所以要保留 onChangeText 过滤器。

🌐 Setting keyboardType (for example, number-pad, decimal-pad, or phone-pad) shows a matching keyboard, but it does not enforce anything: hardware keyboards and pasted text can still insert other characters, so keep the onChangeText filter.

强制大写输入

🌐 Force uppercase input

强制大写和格式化是同样的模式。把这个转换和 autoCapitalize 配对,这样屏幕键盘一开始就会输入大写字母。

🌐 Forcing uppercase is the same pattern as formatting. Pair the transform with autoCapitalize so the on-screen keyboard produces capital letters to begin with.

下面的例子将优惠码存成大写:

🌐 The following example stores a coupon code in uppercase:

<TextInput value={code} onChangeText={text => setCode(text.toUpperCase())} autoCapitalize="characters" placeholder="COUPON" />

在格式化时保持光标位置不变

🌐 Keep the cursor in place while formatting

在 React Native 的新架构出现之前,重新格式化 onChangeText 里的值会把光标跳到字段的末尾,这让在输入中间编辑变得很困难。

🌐 Before React Native's New Architecture, reformatting the value inside onChangeText snapped the cursor to the end of the field, which made edits in the middle of the input difficult.

在使用新架构的 React Native 和 Expo 应用中,只要 onChangeText 返回的文本没有变化,在字段中间输入时光标就会保持原位。当返回经过转换的文本时,本地字段必须将旧光标位置映射到新字符串上,而当转换插入或删除字符时,这种映射可能会出错。

🌐 In React Native and Expo apps that use the New Architecture, typing in the middle of the field keeps the cursor in place as long as onChangeText passes the text back unchanged. When it returns transformed text, the native field has to map the old cursor position onto the new string, and that mapping can miss when the transform inserts or removes characters.

在格式化时保持光标稳定:

🌐 To keep the cursor stable while formatting:

用 worklet 消除格式闪烁

🌐 Remove formatting flicker with worklets

在 JavaScript 中,格式化会经历状态的往返和一次重新渲染,然后本地字段才会更新,所以字段可能会短暂显示原始文本,然后才被格式化后的值替换。来自 @expo/ui 的通用 TextInput 消除了这种往返。它的 value 是用 useNativeState 创建的可观察状态对象,而 onChangeText 可以是一个在 UI 线程上同步运行的 worklet。格式化后的值会与按键操作在同一帧内显示。

🌐 Formatting in JavaScript round-trips through state and a re-render before the native field updates, so the field can briefly show the raw text before the formatted value replaces it. The universal TextInput from @expo/ui removes that round-trip. Its value is an observable state object created with useNativeState, and onChangeText can be a worklet that runs synchronously on the UI thread. The formatted value lands in the same frame as the keystroke.

下图显示了工作单元路径上的同一按键操作:

🌐 The following diagram shows the same keystroke on the worklet path:

Native/UI thread
1

Keystroke

2

onChangeText worklet formats the string

3

Worklet writes value and selection together

4

Field shows the formatted text in the same frame

JavaScript thread: not involved in this update

以下示例会在用户输入时在 UI 线程上格式化电话号码。它需要:

🌐 The following example formats a phone number on the UI thread as the user types. It requires:

import { Host, TextInput, useNativeState } from '@expo/ui'; import { useCallback } from 'react'; function formatPhone(input: string) { 'worklet'; const digits = input.replace(/\D/g, '').slice(0, 10); if (digits.length <= 3) return digits; if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`; return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } export default function PhoneMaskExample() { const phone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); const handleChangeText = useCallback( (value: string) => { 'worklet'; const formatted = formatPhone(value); if (formatted !== value) { phone.value = formatted; // Snaps to end for demo. Real masks need smarter cursor handling. selection.value = { start: formatted.length, end: formatted.length }; } }, [phone, selection] ); return ( <Host matchContents={{ vertical: true }}> <TextInput value={phone} selection={selection} keyboardType="phone-pad" placeholder="(555) 123-4567" onChangeText={handleChangeText} /> </Host> ); }

formatted !== value 检查会在格式化没有改变文本时跳过重写。当掩码确实重写了字符串时,就把 selection.value 和 phone.value 一起写上。否则,光标就会与重写后的文本不同步,快速输入时字符会出现在错误的位置。当用户在字段末尾输入时,把光标移到末尾是最简单的正确做法。一个支持中间编辑的正式掩码需要更智能的光标处理方式。

🌐 The formatted !== value check skips the rewrite when formatting leaves the text unchanged. When the mask does rewrite the string, write selection.value together with phone.value. Otherwise, the cursor falls out of sync with the rewritten text, and fast typing lands characters in the wrong position. Moving the cursor to the end is the simplest correct behavior when the user types at the end of the field. A production mask that supports mid-string edits needs smarter cursor handling.

@expo/ui 的平台特定文本字段支持相同的受控和工作器模式。请参阅 Jetpack Compose 和 SwiftUI 的受控文本字段示例,以及通用 TextInput 的 工作器掩码示例。

🌐 The platform-specific text fields from @expo/ui support the same controlled and worklet patterns. See the controlled text field examples for Jetpack Compose and SwiftUI, and the worklet masking example for the universal TextInput.

什么时候使用库

🌐 When to use a library

手工处理格式只适合一两个字段。使用库可以处理光标计算和一些特殊情况的本地化规则,比如国际电话号码、货币分隔符、信用卡号码和日期:

🌐 Hand-rolled formatting works for one or two fields. A library handles the cursor math and locale rules for edge cases such as international phone numbers, currency separators, credit card numbers, and dates:

控制其他组件

🌐 Control other components

受控模式不仅限于文本输入。任何将 value 属性与 change 回调配对的组件,都遵循和 TextInput 相同的模式。

🌐 The controlled pattern extends beyond text inputs. Any component that pairs a value prop with a change callback follows the same pattern as TextInput.

Switch 是最严格的例子。当你传入 value 时,TextInput 会选择进入受控模式,但 Switch 总是受控的。除非 onValueChange 更新 value 属性,否则开关会恢复到你传入的值。你可以把状态传给 value,并从 onValueChange 更新它,onValueChange 返回的是布尔值而不是字符串:

import { useState } from 'react'; import { Switch } from 'react-native'; export default function NotificationsToggle() { const [enabled, setEnabled] = useState(false); return <Switch value={enabled} onValueChange={setEnabled} />; }

这个模式在其他组件中也会出现:

🌐 The same pattern appears across other components:

  • RefreshControl 把 refreshing 当作受控属性。在 onRefresh 里把它设置为 true,刷新结束后再改回 false。如果它一直没变,指示器会立刻停止。
  • expo-checkbox 的 Checkbox 将 value 与 onValueChange 配对。
  • @react-native-picker/picker 的 Picker 将 selectedValue 与 onValueChange 配对。

这些组件每次交互只发出一个独立的值,所以格式化文本时出现的光标和闪烁问题不会出现。

🌐 These components emit one discrete value per interaction, so the cursor and flicker problems that come with formatting text do not apply.

其他资源

🌐 Additional resources