【React Native娓娓道来】第7课 表单与用户输入:TextInput、表单校验与键盘处理
开篇:让用户把数据填进来
前六课的应用大多是"展示型"的:显示文本、渲染列表、页面跳转。数据要么写死,要么从服务器拉。
但真实应用离不开"用户输入"。登录要填账号密码,注册要填手机号邮箱,发布动态要写文字,搜索要输入关键词。没有输入,App 就只是一个单向的信息展示板。
React Native 提供了 TextInput 处理用户输入。听起来简单:放一个输入框,用户打字,你拿到内容。但真做起来,问题一个接一个:
- 输入框的内容谁来管?组件自己管,还是父组件管?
- 用户输入变化时,怎么实时获取?
- 怎么校验?邮箱格式不对、密码太短、两次密码不一致,怎么提示?
- 键盘弹出来后,挡住了输入框怎么办?
- 多个输入框之间,怎么从上一个跳到下一个?
- 怎么点击空白处收起键盘?
这一课把这些"坑"一个一个填平。
一、TextInput 基础
1.1 最基础的输入框
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { View, TextInput, Text } from 'react-native'; // 引入组件
export default function App() { // 定义并默认导出 App
const [text, setText] = useState(''); // 创建状态 text,初始空字符串
return ( // 返回界面描述
<View style={{ padding: 20 }}> // 外层容器
<TextInput // 输入框
value={text} // 值由 text 决定
onChangeText={setText} // 输入变化时更新 text
placeholder="请输入内容" // 占位提示
style={{ borderWidth: 1, borderColor: '#ccc', borderRadius: 6, paddingHorizontal: 10, height: 40 }} // 样式
/> // 结束 TextInput
<Text>你输入了:{text}</Text> // 显示输入内容
</View> // 结束容器
); // 结束 return
} // 结束 App
三个关键点:
value={text}:输入框显示的内容由text状态决定;onChangeText={setText}:用户输入时,text更新;placeholder:输入框为空时显示的提示文字。
这个模式叫受控组件:输入框的值由状态控制,输入变化更新状态,状态变化又反过来更新输入框。数据和界面始终同步。
1.2 受控 vs 非受控
不传 value 就是非受控。它自己记住输入内容,你只能通过 onChangeText 拿到值,但无法从外部控制它显示什么。
// JavaScript + React Native (JSX)
<TextInput onChangeText={(text) => console.log(text)} /> // 非受控:不传 value
大多数情况下用受控。只有受控才能实时校验、程序化清空或填充、根据其他状态动态改变输入框内容。
1.3 常用属性速查
| 属性 | 作用 | 典型场景 |
|---|---|---|
keyboardType | 键盘类型 | 数字、邮箱、电话 |
secureTextEntry | 隐藏输入 | 密码 |
multiline | 多行输入 | 简介、评论 |
autoCapitalize | 自动大写 | 邮箱设为 none |
autoCorrect | 自动纠错 | 邮箱、密码设为 false |
maxLength | 最大长度 | 手机号、验证码 |
editable | 是否可编辑 | 只读展示 |
returnKeyType | 回车键文字 | next、done、search |
几个最常用的示例:
// JavaScript + React Native (JSX)
<TextInput keyboardType="numeric" /> // 数字键盘
<TextInput keyboardType="email-address" /> // 邮箱键盘
<TextInput secureTextEntry /> // 密码输入
<TextInput multiline numberOfLines={4} /> // 多行输入
<TextInput autoCapitalize="none" autoCorrect={false} /> // 不自动大写、不纠错
<TextInput maxLength={11} /> // 最多 11 个字符
多行输入的 Android 细节:Android 上多行输入建议设置 textAlignVertical: 'top',让文字从顶部开始:
// JavaScript + React Native (JSX)
<TextInput multiline style={{ height: 100, textAlignVertical: 'top' }} />
1.4 焦点与提交
// JavaScript + React Native (JSX)
<TextInput // 输入框
onFocus={() => console.log('获得焦点')} // 获得焦点时触发
onBlur={() => console.log('失去焦点')} // 失去焦点时触发
onSubmitEditing={() => console.log('按了回车')} // 按下提交键时触发
/>
onFocus 和 onBlur 常用于:获得焦点时改变边框颜色;失去焦点时触发校验。
二、表单状态管理
2.1 从多个 useState 到对象状态
字段少的时候,每个字段一个状态:
// JavaScript + React Native
const [username, setUsername] = useState(''); // 用户名
const [email, setEmail] = useState(''); // 邮箱
const [password, setPassword] = useState(''); // 密码
字段一多就显得零散。用一个对象状态管理:
// JavaScript + React Native
const [form, setForm] = useState({ // 对象状态
username: '', // 用户名
email: '', // 邮箱
password: '', // 密码
}); // 结束 useState
const updateField = (field, value) => { // 定义字段更新函数
setForm((prev) => ({ ...prev, [field]: value })); // 用展开运算符和计算属性名更新
}; // 结束 updateField
使用时:
// JavaScript + React Native (JSX)
<TextInput // 输入框
value={form.username} // 值由 form.username 决定
onChangeText={(text) => updateField('username', text)} // 输入变化时更新
/>
updateField 用展开运算符创建新对象,覆盖指定字段,符合不可变更新原则。
2.2 封装成自定义 Hook
字段再多一点,抽一个 useForm:
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
function useForm(initialValues) { // 定义 useForm Hook
const [values, setValues] = useState(initialValues); // 创建状态 values
const handleChange = (field, value) => { // 定义字段更新函数
setValues((prev) => ({ ...prev, [field]: value })); // 用展开运算符更新
}; // 结束 handleChange
const reset = () => setValues(initialValues); // 定义重置函数
return { values, handleChange, reset }; // 返回接口
} // 结束 useForm
使用:
// JavaScript + React Native
const { values, handleChange, reset } = useForm({ // 使用 useForm
username: '', // 用户名
email: '', // 邮箱
password: '', // 密码
}); // 结束 useForm
自定义 Hook 把"表单状态管理"抽出来,组件代码更干净。
2.3 非受控方式:useRef
// JavaScript + React Native
import { useRef } from 'react'; // 引入 useRef
const inputRef = useRef(null); // 创建 ref
<TextInput ref={inputRef} />; // 绑定 ref
const value = inputRef.current.value; // 读取值
inputRef.current.clear(); // 清空
inputRef.current.focus(); // 聚焦
非受控方式适合简单场景,但灵活性和可控性不如受控组件。大多数表单还是推荐受控。
三、表单校验
3.1 两种校验时机
即时校验:每次输入变化都校验。反馈及时,但错误提示可能在用户还没输完时就弹出来,体验不好。
提交时校验:用户点击提交按钮才校验。不会过早打断用户,但反馈滞后,用户可能要重填多次。
实际项目常常结合:输入过程中不显示错误,离开输入框(onBlur)或提交时才校验。
3.2 错误状态管理
// JavaScript + React Native
const [errors, setErrors] = useState({}); // 错误状态,初始空对象
errors 是一个对象,字段名对应错误信息。没有错误的字段就是 undefined。
3.3 常见校验规则
// JavaScript
const isRequired = (value) => value.trim() !== ''; // 非空校验
const isEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); // 邮箱校验
const isPhone = (value) => /^1[3-9]\d{9}$/.test(value); // 手机号校验(中国大陆)
const isPasswordValid = (value) => value.length >= 6; // 密码长度校验
const isPasswordMatch = (p1, p2) => p1 === p2; // 两次密码一致校验
3.4 一个完整的校验函数
// JavaScript + React Native
function validate(values) { // 定义校验函数
const errors = {}; // 创建错误对象
if (!values.username.trim()) { // 用户名为空
errors.username = '用户名不能为空'; // 记录错误
} // 结束判断
if (!values.email.trim()) { // 邮箱为空
errors.email = '邮箱不能为空'; // 记录错误
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { // 邮箱格式不对
errors.email = '邮箱格式不正确'; // 记录错误
} // 结束判断
if (!values.password) { // 密码为空
errors.password = '密码不能为空'; // 记录错误
} else if (values.password.length < 6) { // 密码太短
errors.password = '密码至少 6 位'; // 记录错误
} // 结束判断
if (values.password !== values.confirmPassword) { // 两次密码不一致
errors.confirmPassword = '两次密码不一致'; // 记录错误
} // 结束判断
return errors; // 返回错误对象
} // 结束 validate
这个函数是纯函数:接收表单数据,返回错误对象。不依赖任何外部状态,容易测试。
3.5 显示错误信息
// JavaScript + React Native (JSX)
<TextInput // 输入框
value={values.email} // 值由 values.email 决定
onChangeText={(text) => handleChange('email', text)} // 输入变化时更新
onBlur={() => handleBlur('email')} // 失去焦点时校验
style={[styles.input, errors.email && styles.inputError]} // 有错误时叠加红色边框
/> // 结束 TextInput
{errors.email && <Text style={styles.errorText}>{errors.email}</Text>} // 有错误时才渲染错误文本
style={[styles.input, errors.email && styles.inputError]}:有错误时叠加红色边框样式;{errors.email && <Text>...}:有错误时才渲染错误文本。
3.6 提交时校验
// JavaScript + React Native
const handleSubmit = () => { // 定义提交函数
const validationErrors = validate(values); // 执行校验
setErrors(validationErrors); // 更新错误状态
if (Object.keys(validationErrors).length === 0) { // 没有错误
console.log('提交', values); // 提交表单
} // 结束判断
}; // 结束 handleSubmit
Object.keys(validationErrors).length === 0 表示没有错误,可以提交。
四、键盘处理
4.1 键盘遮挡:KeyboardAvoidingView
键盘弹出来后,会占据屏幕下半部分。如果输入框在下方,就可能被挡住。
// JavaScript + React Native
import { KeyboardAvoidingView, Platform } from 'react-native'; // 引入组件
<KeyboardAvoidingView // 键盘避让容器
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} // iOS 用 padding,Android 用 height
style={{ flex: 1 }} // 占满屏幕
> // 开始标签
{/* 表单内容 */}
</KeyboardAvoidingView> // 结束容器
4.2 收起键盘:Keyboard.dismiss
// JavaScript + React Native
import { Pressable, Keyboard } from 'react-native'; // 引入组件
<Pressable style={{ flex: 1 }} onPress={Keyboard.dismiss}> // 点击时收起键盘
{/* 表单内容 */}
</Pressable> // 结束 Pressable
Keyboard.dismiss 会收起键盘。Pressable 包裹整个页面,点击任意空白处都能收起键盘。
4.3 滚动表单:keyboardShouldPersistTaps
表单放在 ScrollView 中时,键盘弹出后点击输入框可能会先触发滚动,导致键盘收起来。
// JavaScript + React Native (JSX)
<ScrollView // 可滚动容器
keyboardShouldPersistTaps="handled" // 点击可处理组件时不收键盘
contentContainerStyle={{ padding: 20 }} // 内容内边距
> // 开始标签
{/* 表单内容 */}
</ScrollView> // 结束容器
handled 表示:点击可处理点击的组件(比如按钮)时,键盘不收起;点击空白处则收起。
4.4 输入框之间跳转
用 returnKeyType="next" 配合 onSubmitEditing 和 useRef:
// JavaScript + React Native
import { useRef, useState } from 'react'; // 引入 useRef 和 useState
import { View, TextInput } from 'react-native'; // 引入组件
export default function App() { // 定义并默认导出 App
const [username, setUsername] = useState(''); // 用户名状态
const [email, setEmail] = useState(''); // 邮箱状态
const emailRef = useRef(null); // 创建 ref 指向邮箱输入框
return ( // 返回界面描述
<View style={{ padding: 20 }}> // 外层容器
<TextInput // 用户名输入框
value={username} // 值由 username 决定
onChangeText={setUsername} // 输入变化时更新
placeholder="用户名" // 占位提示
returnKeyType="next" // 回车键显示"下一项"
onSubmitEditing={() => emailRef.current?.focus()} // 回车时聚焦邮箱输入框
style={{ borderWidth: 1, borderColor: '#ccc', height: 40, marginBottom: 12, paddingHorizontal: 10 }} // 样式
/> // 结束用户名输入框
<TextInput // 邮箱输入框
ref={emailRef} // 绑定 ref
value={email} // 值由 email 决定
onChangeText={setEmail} // 输入变化时更新
placeholder="邮箱" // 占位提示
keyboardType="email-address" // 邮箱键盘
autoCapitalize="none" // 不自动大写
returnKeyType="done" // 回车键显示"完成"
style={{ borderWidth: 1, borderColor: '#ccc', height: 40, paddingHorizontal: 10 }} // 样式
/> // 结束邮箱输入框
</View> // 结束容器
); // 结束 return
} // 结束 App
用户在"用户名"里按回车,焦点跳到"邮箱"。
4.5 Keyboard API
Keyboard 模块提供了监听键盘事件的能力:
// JavaScript + React Native
import { useEffect } from 'react'; // 引入 useEffect
import { Keyboard } from 'react-native'; // 引入 Keyboard
useEffect(() => { // 注册副作用
const showSub = Keyboard.addListener('keyboardDidShow', () => { // 监听键盘弹出
console.log('键盘弹出'); // 打印日志
}); // 结束监听
const hideSub = Keyboard.addListener('keyboardDidHide', () => { // 监听键盘收起
console.log('键盘收起'); // 打印日志
}); // 结束监听
return () => { // 返回清理函数
showSub.remove(); // 取消弹出监听
hideSub.remove(); // 取消收起监听
}; // 结束清理
}, []); // 空数组
订阅键盘事件也要记得在组件卸载时取消订阅,这是第4课讲的清理函数。
五、完整示例:注册表单
把前面所有知识点组合起来,做一个完整的注册表单。
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { // 引入需要的组件
View, // 容器
Text, // 文本
TextInput, // 输入框
Button, // 按钮
ScrollView, // 可滚动容器
KeyboardAvoidingView, // 键盘避让
Platform, // 平台判断
Pressable, // 可点击容器
Keyboard, // 键盘 API
StyleSheet, // 样式表
} from 'react-native'; // 从 react-native 导入
function validate(values) { // 定义校验函数
const errors = {}; // 创建错误对象
if (!values.username.trim()) errors.username = '用户名不能为空'; // 用户名不能为空
if (!values.email.trim()) { // 邮箱为空
errors.email = '邮箱不能为空'; // 记录错误
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { // 邮箱格式不对
errors.email = '邮箱格式不正确'; // 记录错误
} // 结束判断
if (!values.password) { // 密码为空
errors.password = '密码不能为空'; // 记录错误
} else if (values.password.length < 6) { // 密码太短
errors.password = '密码至少 6 位'; // 记录错误
} // 结束判断
if (values.password !== values.confirmPassword) { // 两次密码不一致
errors.confirmPassword = '两次密码不一致'; // 记录错误
} // 结束判断
return errors; // 返回错误对象
} // 结束 validate
export default function App() { // 定义并默认导出 App
const [values, setValues] = useState({ // 创建表单状态
username: '', // 用户名
email: '', // 邮箱
password: '', // 密码
confirmPassword: '', // 确认密码
}); // 结束 useState
const [errors, setErrors] = useState({}); // 创建错误状态
const handleChange = (field, value) => { // 定义字段更新函数
setValues((prev) => ({ ...prev, [field]: value })); // 用展开运算符更新
}; // 结束 handleChange
const handleSubmit = () => { // 定义提交函数
const validationErrors = validate(values); // 执行校验
setErrors(validationErrors); // 更新错误状态
if (Object.keys(validationErrors).length === 0) { // 没有错误
console.log('提交成功', values); // 提交表单
} // 结束判断
}; // 结束 handleSubmit
const renderInput = (field, placeholder, options = {}) => ( // 定义输入框渲染函数
<View style={styles.field}> // 字段容器
<TextInput // 输入框
style={[styles.input, errors[field] && styles.inputError]} // 有错误时叠加红色边框
value={values[field]} // 值由 values[field] 决定
onChangeText={(text) => handleChange(field, text)} // 输入变化时更新
placeholder={placeholder} // 占位提示
{...options} // 展开其他属性
/> // 结束 TextInput
{errors[field] && <Text style={styles.errorText}>{errors[field]}</Text>} // 有错误时显示错误文本
</View> // 结束字段容器
); // 结束 renderInput
return ( // 返回界面描述
<KeyboardAvoidingView // 键盘避让容器
style={{ flex: 1 }} // 占满屏幕
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} // 平台不同行为不同
> // 结束开始标签
<Pressable style={{ flex: 1 }} onPress={Keyboard.dismiss}> // 点击空白处收起键盘
<ScrollView // 可滚动容器
contentContainerStyle={styles.container} // 内容样式
keyboardShouldPersistTaps="handled" // 点击输入框不收键盘
> // 结束开始标签
<Text style={styles.heading}>注册</Text> // 标题
{renderInput('username', '用户名', { autoCapitalize: 'none' })} // 用户名输入框
{renderInput('email', '邮箱', { // 邮箱输入框
keyboardType: 'email-address', // 邮箱键盘
autoCapitalize: 'none', // 不自动大写
})} // 结束邮箱
{renderInput('password', '密码(至少 6 位)', { // 密码输入框
secureTextEntry: true, // 隐藏输入内容
})} // 结束密码
{renderInput('confirmPassword', '确认密码', { // 确认密码输入框
secureTextEntry: true, // 隐藏输入内容
})} // 结束确认密码
<Button title="提交" onPress={handleSubmit} /> // 提交按钮
</ScrollView> // 结束滚动容器
</Pressable> // 结束 Pressable
</KeyboardAvoidingView> // 结束键盘避让
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { padding: 20, paddingTop: 60 }, // 内容容器样式
heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 }, // 标题样式
field: { marginBottom: 16 }, // 字段容器样式
input: { // 输入框样式
borderWidth: 1, // 边框宽度 1
borderColor: '#ccc', // 边框颜色
borderRadius: 6, // 圆角 6
paddingHorizontal: 12, // 水平内边距 12
height: 44, // 高度 44
fontSize: 16, // 字号 16
}, // 结束 input 样式
inputError: { borderColor: '#d00' }, // 错误时边框红色
errorText: { color: '#d00', fontSize: 13, marginTop: 4 }, // 错误文本样式
}); // 结束样式表
这个例子综合运用了:对象状态管理多个字段;handleChange 不可变更新;validate 纯函数校验;提交时校验;错误状态和错误样式;KeyboardAvoidingView 避免键盘遮挡;Pressable + Keyboard.dismiss 点击空白收起键盘;ScrollView + keyboardShouldPersistTaps 保证滚动时点击输入框不会误收键盘。
六、表单与"视图 = 函数(状态)"
表单是"视图 = 函数(状态)"最直观的体现之一:
- 输入框显示什么,由
values决定; - 错误提示是否显示,由
errors决定; - 按钮是否可用,由校验结果决定;
- 用户每次输入,更新
values,触发重新渲染,界面和状态始终同步。
// 概念表达
表单视图 = 函数(values, errors) // 表单由值和错误决定
你写的是 value={values.email},而不是"找到输入框,设置它的值"。用户输入不是"直接改界面",而是"更新状态"。状态变,界面重新计算。
七、练习题
练习题1:选择题——受控组件
题目:下面哪一项是受控组件的标志?
A. 不传 value,只传 onChangeText
B. 传 value 和 onChangeText
C. 只用 ref 读取值
D. 只传 placeholder
参考答案:B
解读:受控组件的值由状态控制,所以必须传 value,同时通过 onChangeText 更新状态。不传 value 是非受控组件。
练习题2:填空题——更新对象状态中的字段
题目:补全代码,使 handleChange('email', 'a@b.com') 能更新 values.email。
// JavaScript + React Native
const [values, setValues] = useState({ email: '' }); // 创建对象状态
const handleChange = (field, value) => { // 定义更新函数
setValues((prev) => ({ ____ })); // 补全更新逻辑
}; // 结束 handleChange
参考答案:...prev, [field]: value
解读:用展开运算符复制原对象,再用计算属性名 [field] 覆盖指定字段。这是不可变更新对象的标准写法。
练习题3:改错题——直接修改状态
题目:下面代码有一处错误,请指出并修正。
// JavaScript + React Native
const handleChange = (field, value) => { // 定义更新函数
values[field] = value; // 直接修改对象
setValues(values); // 传回同一个引用
}; // 结束 handleChange
参考答案:
// JavaScript + React Native
const handleChange = (field, value) => { // 定义更新函数
setValues((prev) => ({ ...prev, [field]: value })); // 创建新对象
}; // 结束 handleChange
解读:直接修改 values 对象,引用没变,React Native 可能不会更新。应该创建新对象。
练习题4:代码阅读题——错误显示
题目:阅读代码,说出 errors.email 为 '邮箱格式不正确' 时,界面会显示什么。
// JavaScript + React Native (JSX)
<TextInput style={[styles.input, errors.email && styles.inputError]} /> // 输入框
{errors.email && <Text style={styles.errorText}>{errors.email}</Text>} // 错误文本
参考答案:输入框会叠加红色边框样式,下方显示红色文字"邮箱格式不正确"。
解读:errors.email && styles.inputError 有错误时返回样式对象,无错误时返回 undefined(在样式数组中会被忽略)。errors.email && <Text> 有错误时才渲染文本。
练习题5:动手题——密码输入框
题目:创建一个密码输入框,输入内容显示为圆点。
参考答案:
// JavaScript + React Native (JSX)
<TextInput // 密码输入框
secureTextEntry // 隐藏输入内容
value={password} // 值由 password 决定
onChangeText={setPassword} // 输入变化时更新 password
placeholder="请输入密码" // 占位提示
/>
解读:secureTextEntry 让输入内容显示为圆点,适合密码。iOS 和 Android 都支持。
练习题6:简答题——受控和非受控的区别
题目:简述受控组件和非受控组件的区别。
参考答案:受控组件的值由 React 状态控制,通过 value 和 onChangeText 实现双向同步;非受控组件的值由组件自己管理,通常通过 ref 读取。
解读:受控组件更适合需要实时校验、动态控制的场景。非受控组件适合简单场景。
练习题7:校验规则题——邮箱
题目:写一个函数,判断字符串是否是合法邮箱。
参考答案:
// JavaScript
const isEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); // 邮箱正则校验
解读:这是一个简化的邮箱正则,能过滤大多数明显错误的格式。生产环境中可以用更严格的正则或专门的库。
练习题8:校验函数题
题目:写一个 validate 函数,校验 username 不能为空,password 至少 6 位。
参考答案:
// JavaScript + React Native
function validate(values) { // 定义校验函数
const errors = {}; // 创建错误对象
if (!values.username.trim()) { // 用户名为空
errors.username = '用户名不能为空'; // 记录错误
} // 结束判断
if (!values.password || values.password.length < 6) { // 密码为空或太短
errors.password = '密码至少 6 位'; // 记录错误
} // 结束判断
return errors; // 返回错误对象
} // 结束 validate
解读:validate 是纯函数,输入表单数据,输出错误对象。容易测试,也容易复用。
练习题9:提交校验题
题目:写一个 handleSubmit,先校验,通过后打印表单数据。
参考答案:
// JavaScript + React Native
const handleSubmit = () => { // 定义提交函数
const errors = validate(values); // 执行校验
setErrors(errors); // 更新错误状态
if (Object.keys(errors).length === 0) { // 没有错误
console.log('提交', values); // 提交表单
} // 结束判断
}; // 结束 handleSubmit
解读:Object.keys(errors).length === 0 表示没有错误。校验通过才提交。
练习题10:键盘避让题
题目:用 KeyboardAvoidingView 包裹表单,避免键盘遮挡输入框。
参考答案:
// JavaScript + React Native
import { KeyboardAvoidingView, Platform } from 'react-native'; // 引入组件
<KeyboardAvoidingView // 键盘避让容器
style={{ flex: 1 }} // 占满屏幕
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} // 平台不同行为不同
> // 开始标签
{/* 表单内容 */}
</KeyboardAvoidingView> // 结束容器
解读:iOS 推荐 padding,Android 推荐 height 或 padding。它会在键盘弹出时调整布局,避免输入框被遮挡。
练习题11:收起键盘题
题目:点击页面空白处收起键盘。
参考答案:
// JavaScript + React Native
import { Pressable, Keyboard } from 'react-native'; // 引入组件
<Pressable style={{ flex: 1 }} onPress={Keyboard.dismiss}> // 点击时收起键盘
{/* 表单内容 */}
</Pressable> // 结束 Pressable
解读:Keyboard.dismiss 收起键盘。Pressable 包裹整个页面,点击空白处即可触发。
练习题12:输入框跳转题
题目:有两个输入框,用户在第一个按回车时,焦点跳到第二个。
参考答案:
// JavaScript + React Native
import { useRef } from 'react'; // 引入 useRef
const secondRef = useRef(null); // 创建 ref 指向第二个输入框
<TextInput // 第一个输入框
returnKeyType="next" // 回车键显示"下一项"
onSubmitEditing={() => secondRef.current?.focus()} // 回车时聚焦第二个输入框
/>
<TextInput ref={secondRef} returnKeyType="done" /> // 第二个输入框
解读:returnKeyType="next" 让回车键显示"下一项"。onSubmitEditing 中调用 focus() 让下一个输入框获得焦点。
练习题13:ScrollView 键盘题
题目:表单放在 ScrollView 中,点击输入框时键盘总是先收起来。如何解决?
参考答案:给 ScrollView 加 keyboardShouldPersistTaps="handled"。
解读:handled 表示点击可处理点击的组件时不收起键盘,点击空白处才收起。这是表单放在 ScrollView 中时的常见问题。
练习题14:综合题——登录表单
题目:创建一个登录表单,包含邮箱和密码,提交时校验:邮箱不能为空且格式正确,密码不能为空。
参考答案:
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { View, Text, TextInput, Button, StyleSheet } from 'react-native'; // 引入需要的组件
function validate(values) { // 定义校验函数
const errors = {}; // 创建错误对象
if (!values.email.trim()) { // 邮箱为空
errors.email = '邮箱不能为空'; // 记录错误
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { // 邮箱格式不对
errors.email = '邮箱格式不正确'; // 记录错误
} // 结束判断
if (!values.password) { // 密码为空
errors.password = '密码不能为空'; // 记录错误
} // 结束判断
return errors; // 返回错误对象
} // 结束 validate
export default function App() { // 定义并默认导出 App
const [values, setValues] = useState({ email: '', password: '' }); // 创建表单状态
const [errors, setErrors] = useState({}); // 创建错误状态
const handleChange = (field, value) => { // 定义字段更新函数
setValues((prev) => ({ ...prev, [field]: value })); // 用展开运算符更新
}; // 结束 handleChange
const handleSubmit = () => { // 定义提交函数
const validationErrors = validate(values); // 执行校验
setErrors(validationErrors); // 更新错误状态
if (Object.keys(validationErrors).length === 0) { // 没有错误
console.log('登录', values); // 提交表单
} // 结束判断
}; // 结束 handleSubmit
return ( // 返回界面描述
<View style={styles.container}> // 外层容器
<TextInput // 邮箱输入框
style={[styles.input, errors.email && styles.inputError]} // 有错误时叠加红色边框
value={values.email} // 值由 values.email 决定
onChangeText={(t) => handleChange('email', t)} // 输入变化时更新
placeholder="邮箱" // 占位提示
keyboardType="email-address" // 邮箱键盘
autoCapitalize="none" // 不自动大写
/> // 结束邮箱输入框
{errors.email && <Text style={styles.error}>{errors.email}</Text>} // 邮箱错误信息
<TextInput // 密码输入框
style={[styles.input, errors.password && styles.inputError]} // 有错误时叠加红色边框
value={values.password} // 值由 values.password 决定
onChangeText={(t) => handleChange('password', t)} // 输入变化时更新
placeholder="密码" // 占位提示
secureTextEntry // 隐藏输入内容
/> // 结束密码输入框
{errors.password && <Text style={styles.error}>{errors.password}</Text>} // 密码错误信息
<Button title="登录" onPress={handleSubmit} /> // 登录按钮
</View> // 结束容器
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, padding: 20, paddingTop: 60 }, // 容器样式
input: { // 输入框样式
borderWidth: 1, // 边框宽度 1
borderColor: '#ccc', // 边框颜色
borderRadius: 6, // 圆角 6
paddingHorizontal: 12, // 水平内边距 12
height: 44, // 高度 44
marginBottom: 4, // 底部间距 4
}, // 结束 input 样式
inputError: { borderColor: '#d00' }, // 错误时边框红色
error: { color: '#d00', fontSize: 13, marginBottom: 12 }, // 错误文本样式
}); // 结束样式表
解读:这是表单的完整形态:对象状态、不可变更新、纯函数校验、错误显示、提交时校验。掌握这个模板,大多数表单都能应付。
练习题15:综合应用题——带实时校验的注册表单
题目:创建一个注册表单,包含用户名、邮箱、密码。要求:
- 邮箱输入后实时校验格式,不合法时显示错误;
- 密码长度不足 6 位时显示错误;
- 用户名不能为空;
- 提交时统一校验,通过后打印数据。
参考答案:
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { // 引入需要的组件
View, // 容器
Text, // 文本
TextInput, // 输入框
Button, // 按钮
ScrollView, // 可滚动容器
KeyboardAvoidingView, // 键盘避让
Platform, // 平台判断
Pressable, // 可点击容器
Keyboard, // 键盘 API
StyleSheet, // 样式表
} from 'react-native'; // 从 react-native 导入
function validateField(field, value) { // 定义单字段校验函数
switch (field) { // 根据字段名分发
case 'username': // 用户名
if (!value.trim()) return '用户名不能为空'; // 不能为空
return ''; // 校验通过
case 'email': // 邮箱
if (!value.trim()) return '邮箱不能为空'; // 不能为空
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return '邮箱格式不正确'; // 格式校验
return ''; // 校验通过
case 'password': // 密码
if (!value) return '密码不能为空'; // 不能为空
if (value.length < 6) return '密码至少 6 位'; // 长度校验
return ''; // 校验通过
default: // 其他
return ''; // 返回空
} // 结束 switch
} // 结束 validateField
export default function App() { // 定义并默认导出 App
const [values, setValues] = useState({ // 创建表单状态
username: '', // 用户名
email: '', // 邮箱
password: '', // 密码
}); // 结束 useState
const [errors, setErrors] = useState({}); // 创建错误状态
const handleChange = (field, value) => { // 定义字段更新函数
setValues((prev) => ({ ...prev, [field]: value })); // 用展开运算符更新
const error = validateField(field, value); // 实时校验该字段
setErrors((prev) => ({ ...prev, [field]: error })); // 更新该字段的错误
}; // 结束 handleChange
const handleSubmit = () => { // 定义提交函数
const allErrors = {}; // 创建错误对象
['username', 'email', 'password'].forEach((field) => { // 遍历所有字段
const error = validateField(field, values[field]); // 校验每个字段
if (error) allErrors[field] = error; // 有错误就记录
}); // 结束遍历
setErrors(allErrors); // 更新错误状态
if (Object.keys(allErrors).length === 0) { // 没有错误
console.log('提交', values); // 提交表单
} // 结束判断
}; // 结束 handleSubmit
const renderInput = (field, placeholder, options = {}) => ( // 定义输入框渲染函数
<View style={styles.field}> // 字段容器
<TextInput // 输入框
style={[styles.input, errors[field] && styles.inputError]} // 有错误时叠加红色边框
value={values[field]} // 值由 values[field] 决定
onChangeText={(t) => handleChange(field, t)} // 输入变化时更新
placeholder={placeholder} // 占位提示
{...options} // 展开其他属性
/> // 结束 TextInput
{errors[field] ? <Text style={styles.error}>{errors[field]}</Text> : null} // 有错误时显示错误文本
</View> // 结束字段容器
); // 结束 renderInput
return ( // 返回界面描述
<KeyboardAvoidingView // 键盘避让容器
style={{ flex: 1 }} // 占满屏幕
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} // 平台不同行为不同
> // 结束开始标签
<Pressable style={{ flex: 1 }} onPress={Keyboard.dismiss}> // 点击空白处收起键盘
<ScrollView // 可滚动容器
contentContainerStyle={styles.container} // 内容样式
keyboardShouldPersistTaps="handled" // 点击输入框不收键盘
> // 结束开始标签
<Text style={styles.heading}>注册</Text> // 标题
{renderInput('username', '用户名', { autoCapitalize: 'none' })} // 用户名输入框
{renderInput('email', '邮箱', { // 邮箱输入框
keyboardType: 'email-address', // 邮箱键盘
autoCapitalize: 'none', // 不自动大写
})} // 结束邮箱
{renderInput('password', '密码(至少 6 位)', { // 密码输入框
secureTextEntry: true, // 隐藏输入内容
})} // 结束密码
<Button title="提交" onPress={handleSubmit} /> // 提交按钮
</ScrollView> // 结束滚动容器
</Pressable> // 结束 Pressable
</KeyboardAvoidingView> // 结束键盘避让
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { padding: 20, paddingTop: 60 }, // 内容容器样式
heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 }, // 标题样式
field: { marginBottom: 16 }, // 字段容器样式
input: { // 输入框样式
borderWidth: 1, // 边框宽度 1
borderColor: '#ccc', // 边框颜色
borderRadius: 6, // 圆角 6
paddingHorizontal: 12, // 水平内边距 12
height: 44, // 高度 44
fontSize: 16, // 字号 16
}, // 结束 input 样式
inputError: { borderColor: '#d00' }, // 错误时边框红色
error: { color: '#d00', fontSize: 13, marginTop: 4 }, // 错误文本样式
}); // 结束样式表
解读:这道题在提交校验的基础上增加了实时校验。handleChange 中每次输入都调用 validateField 校验当前字段,只更新该字段的错误信息。提交时再统一校验所有字段,确保没有遗漏。这是体验较好的表单校验模式:输入过程中即时反馈,提交时兜底。
八、总结
这一课我们学习了 React Native 中处理用户输入的完整方案:
TextInput是输入框组件,通过value和onChangeText实现受控组件;- 常用属性:
keyboardType、secureTextEntry、multiline、autoCapitalize、autoCorrect、maxLength; - 表单状态可以用多个
useState,也可以用对象状态 +handleChange; - 自定义 Hook 可以封装表单状态管理;
- 校验函数应该是纯函数,输入表单数据,输出错误对象;
- 校验时机有即时校验、提交时校验,也可以结合两者;
- 错误显示通过条件渲染和样式数组实现;
KeyboardAvoidingView解决键盘遮挡;Keyboard.dismiss收起键盘;keyboardShouldPersistTaps解决ScrollView中键盘收起问题;useRef+onSubmitEditing实现输入框之间跳转。
记住一句话:
表单是"视图 = 函数(状态)"最直观的体现:输入框显示什么、错误提示显不显示,都由状态决定。
第8课预告
下一课,我们进入 【React Native娓娓道来】第8课 样式与布局进阶:Flexbox 深入、响应式设计与平台适配。
更多推荐



所有评论(0)