【React Native娓娓道来】第7课 表单与用户输入:TextInput、表单校验与键盘处理

开篇:让用户把数据填进来

前六课的应用大多是"展示型"的:显示文本、渲染列表、页面跳转。数据要么写死,要么从服务器拉。

但真实应用离不开"用户输入"。登录要填账号密码,注册要填手机号邮箱,发布动态要写文字,搜索要输入关键词。没有输入,App 就只是一个单向的信息展示板。

React Native 提供了 TextInput 处理用户输入。听起来简单:放一个输入框,用户打字,你拿到内容。但真做起来,问题一个接一个:

  • 输入框的内容谁来管?组件自己管,还是父组件管?
  • 用户输入变化时,怎么实时获取?
  • 怎么校验?邮箱格式不对、密码太短、两次密码不一致,怎么提示?
  • 键盘弹出来后,挡住了输入框怎么办?
  • 多个输入框之间,怎么从上一个跳到下一个?
  • 怎么点击空白处收起键盘?

这一课把这些"坑"一个一个填平。


一、TextInput 基础

1.1 最基础的输入框

javascript 复制代码
// 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 复制代码
// 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 复制代码
// 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 复制代码
// JavaScript + React Native (JSX)
<TextInput multiline style={{ height: 100, textAlignVertical: 'top' }} />

1.4 焦点与提交

javascript 复制代码
// JavaScript + React Native (JSX)
<TextInput                                                          // 输入框
  onFocus={() => console.log('获得焦点')}                            // 获得焦点时触发
  onBlur={() => console.log('失去焦点')}                             // 失去焦点时触发
  onSubmitEditing={() => console.log('按了回车')}                    // 按下提交键时触发
/>

onFocus 和 onBlur 常用于:获得焦点时改变边框颜色;失去焦点时触发校验。


二、表单状态管理

2.1 从多个 useState 到对象状态

字段少的时候,每个字段一个状态:

javascript 复制代码
// JavaScript + React Native
const [username, setUsername] = useState('');                       // 用户名
const [email, setEmail] = useState('');                             // 邮箱
const [password, setPassword] = useState('');                       // 密码

字段一多就显得零散。用一个对象状态管理:

javascript 复制代码
// JavaScript + React Native
const [form, setForm] = useState({                                  // 对象状态
  username: '',                                                     // 用户名
  email: '',                                                        // 邮箱
  password: '',                                                     // 密码
});                                                                 // 结束 useState

const updateField = (field, value) => {                             // 定义字段更新函数
  setForm((prev) => ({ ...prev, [field]: value }));                 // 用展开运算符和计算属性名更新
};                                                                  // 结束 updateField

使用时:

javascript 复制代码
// JavaScript + React Native (JSX)
<TextInput                                                          // 输入框
  value={form.username}                                             // 值由 form.username 决定
  onChangeText={(text) => updateField('username', text)}            // 输入变化时更新
/>

updateField 用展开运算符创建新对象,覆盖指定字段,符合不可变更新原则。

2.2 封装成自定义 Hook

字段再多一点,抽一个 useForm:

javascript 复制代码
// 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 复制代码
// JavaScript + React Native
const { values, handleChange, reset } = useForm({                   // 使用 useForm
  username: '',                                                     // 用户名
  email: '',                                                        // 邮箱
  password: '',                                                     // 密码
});                                                                 // 结束 useForm

自定义 Hook 把"表单状态管理"抽出来,组件代码更干净。

2.3 非受控方式:useRef

javascript 复制代码
// 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 复制代码
// JavaScript + React Native
const [errors, setErrors] = useState({});                           // 错误状态,初始空对象

errors 是一个对象,字段名对应错误信息。没有错误的字段就是 undefined。

3.3 常见校验规则

javascript 复制代码
// 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 复制代码
// 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 复制代码
// 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 复制代码
// 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 复制代码
// 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 复制代码
// 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 复制代码
// JavaScript + React Native (JSX)
<ScrollView                                                         // 可滚动容器
  keyboardShouldPersistTaps="handled"                               // 点击可处理组件时不收键盘
  contentContainerStyle={{ padding: 20 }}                           // 内容内边距
>                                                                   // 开始标签
  {/* 表单内容 */}
</ScrollView>                                                       // 结束容器

handled 表示:点击可处理点击的组件(比如按钮)时,键盘不收起;点击空白处则收起。

4.4 输入框之间跳转

用 returnKeyType="next" 配合 onSubmitEditing 和 useRef:

javascript 复制代码
// 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 复制代码
// 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 复制代码
// 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,触发重新渲染,界面和状态始终同步。
text 复制代码
// 概念表达
表单视图 = 函数(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 复制代码
// JavaScript + React Native
const [values, setValues] = useState({ email: '' });                // 创建对象状态

const handleChange = (field, value) => {                            // 定义更新函数
  setValues((prev) => ({ ____ }));                                  // 补全更新逻辑
};                                                                  // 结束 handleChange

参考答案 :...prev, [field]: value

解读 :用展开运算符复制原对象,再用计算属性名 [field] 覆盖指定字段。这是不可变更新对象的标准写法。


练习题3:改错题------直接修改状态

题目:下面代码有一处错误,请指出并修正。

javascript 复制代码
// JavaScript + React Native
const handleChange = (field, value) => {                            // 定义更新函数
  values[field] = value;                                            // 直接修改对象
  setValues(values);                                                // 传回同一个引用
};                                                                  // 结束 handleChange

参考答案:

javascript 复制代码
// JavaScript + React Native
const handleChange = (field, value) => {                            // 定义更新函数
  setValues((prev) => ({ ...prev, [field]: value }));               // 创建新对象
};                                                                  // 结束 handleChange

解读 :直接修改 values 对象,引用没变,React Native 可能不会更新。应该创建新对象。


练习题4:代码阅读题------错误显示

题目 :阅读代码,说出 errors.email 为 '邮箱格式不正确' 时,界面会显示什么。

javascript 复制代码
// 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 复制代码
// JavaScript + React Native (JSX)
<TextInput                                                          // 密码输入框
  secureTextEntry                                                   // 隐藏输入内容
  value={password}                                                  // 值由 password 决定
  onChangeText={setPassword}                                        // 输入变化时更新 password
  placeholder="请输入密码"                                           // 占位提示
/>

解读 :secureTextEntry 让输入内容显示为圆点,适合密码。iOS 和 Android 都支持。


练习题6:简答题------受控和非受控的区别

题目:简述受控组件和非受控组件的区别。

参考答案 :受控组件的值由 React 状态控制,通过 value 和 onChangeText 实现双向同步;非受控组件的值由组件自己管理,通常通过 ref 读取。

解读:受控组件更适合需要实时校验、动态控制的场景。非受控组件适合简单场景。


练习题7:校验规则题------邮箱

题目:写一个函数,判断字符串是否是合法邮箱。

参考答案:

javascript 复制代码
// JavaScript
const isEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); // 邮箱正则校验

解读:这是一个简化的邮箱正则,能过滤大多数明显错误的格式。生产环境中可以用更严格的正则或专门的库。


练习题8:校验函数题

题目 :写一个 validate 函数,校验 username 不能为空,password 至少 6 位。

参考答案:

javascript 复制代码
// 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 复制代码
// 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 复制代码
// 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 复制代码
// JavaScript + React Native
import { Pressable, Keyboard } from 'react-native';                 // 引入组件

<Pressable style={{ flex: 1 }} onPress={Keyboard.dismiss}>          // 点击时收起键盘
  {/* 表单内容 */}
</Pressable>                                                        // 结束 Pressable

解读 :Keyboard.dismiss 收起键盘。Pressable 包裹整个页面,点击空白处即可触发。


练习题12:输入框跳转题

题目:有两个输入框,用户在第一个按回车时,焦点跳到第二个。

参考答案:

javascript 复制代码
// 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 复制代码
// 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:综合应用题------带实时校验的注册表单

题目:创建一个注册表单,包含用户名、邮箱、密码。要求:

  1. 邮箱输入后实时校验格式,不合法时显示错误;
  2. 密码长度不足 6 位时显示错误;
  3. 用户名不能为空;
  4. 提交时统一校验,通过后打印数据。

参考答案:

javascript 复制代码
// 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 中处理用户输入的完整方案:

  1. TextInput 是输入框组件,通过 value 和 onChangeText 实现受控组件;
  2. 常用属性:keyboardType、secureTextEntry、multiline、autoCapitalize、autoCorrect、maxLength;
  3. 表单状态可以用多个 useState,也可以用对象状态 + handleChange;
  4. 自定义 Hook 可以封装表单状态管理;
  5. 校验函数应该是纯函数,输入表单数据,输出错误对象;
  6. 校验时机有即时校验、提交时校验,也可以结合两者;
  7. 错误显示通过条件渲染和样式数组实现;
  8. KeyboardAvoidingView 解决键盘遮挡;
  9. Keyboard.dismiss 收起键盘;
  10. keyboardShouldPersistTaps 解决 ScrollView 中键盘收起问题;
  11. useRef + onSubmitEditing 实现输入框之间跳转。

记住一句话:

表单是"视图 = 函数(状态)"最直观的体现:输入框显示什么、错误提示显不显示,都由状态决定。


第8课预告

下一课,我们进入 【React Native娓娓道来】第8课 样式与布局进阶:Flexbox 深入、响应式设计与平台适配。

相关推荐
传奇开心果编程5 小时前
【React Native娓娓道来】第9课 动画与手势:Animated、Reanimated 与 Pressable 进阶
移动开发·动画·手势·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程6 小时前
【声明式UI开发实用技术学与练】第2课 状态建模:把界面拆成可预测的数据
移动开发·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程7 小时前
【React Native娓娓道来】第3课 组件组合与列表渲染:把界面拆成可复用的函数
移动开发·列表渲染·编程语言理论·开发范式·编程框架·声明式ui·组件组合
传奇开心果编程9 小时前
【声明式UI开发实用技术学与练】第6课 副作用管理:Effect 的正确用法
移动开发·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程11 小时前
【React Native娓娓道来】第4课 副作用与数据获取:useEffect、网络请求与加载状态
移动开发·数据获取·编程语言理论·开发范式·编程框架·副作用·声明式ui
传奇开心果编程15 小时前
【React Native娓娓道来】第10课 数据持久化与本地存储:AsyncStorage、SecureStore 与文件系统
移动开发·数据持久化·本地存储·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程17 小时前
【React Native娓娓道来】第5课 导航与多页面:用 React Navigation 组织页面流
移动开发·导航·编程语言理论·开发范式·编程框架·声明式ui·多页面
传奇开心果编程17 小时前
【SwiftUI提高练中学】第19课 推送与远程通知进阶:APNs、通信通知与专注过滤
移动开发·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程1 天前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui