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:
Keystroke
Native/UI threadField shows the raw text
Native/UI threadonChangeText fires with the string
State update, then re-render
JavaScript threadvalue forces the field to match
与网页上的受控输入的不同
如果你已经写过网页的受控输入,在 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:
用户输入时自动格式化
🌐 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:
- 尽量使用原生属性,例如
maxLength和editable,而不是在 JavaScript 中重新实现它们。参见 限制用户可以输入的内容。 - 使用一个能帮你处理光标计算的掩码库。参见 什么时候使用库。
- 把格式化移动到 UI 线程上。参见 使用 worklets 去除格式化闪烁。
用 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:
Keystroke
onChangeText worklet formats the string
Worklet writes value and selection together
Field shows the formatted text in the same frame
以下示例会在用户输入时在 UI 线程上格式化电话号码。它需要:
🌐 The following example formats a phone number on the UI thread as the user types. It requires:
@expo/ui:通用的TextInput可在 SDK 56 及更高版本中使用。请参阅安装说明。react-native-worklets:Worklet 函数需要它的运行时。
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:
- 用于遮罩:
react-native-mask-input或react-native-mask-text。 - 对于跨多个字段的表单状态和验证:可以使用 React Hook Form 或 Formik。使用 React Hook Form 时,把
TextInput包在它的Controller组件里,因为register不能绑定到原生输入框。
控制其他组件
🌐 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
@expo/ui参考中的TextInput:基于工作单元的输入、它的安装以及完整的属性列表。- 键盘处理:管理屏幕键盘周围的布局。
TextInput在 React Native 文档中:核心组件的完整属性列表。