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

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

但真实应用离不开"用户输入"。登录要填账号密码,注册要填手机号邮箱,发布动态要写文字,搜索要输入关键词。没有输入,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:综合应用题——带实时校验的注册表单

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

  1. 邮箱输入后实时校验格式,不合法时显示错误;
  2. 密码长度不足 6 位时显示错误;
  3. 用户名不能为空;
  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 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 深入、响应式设计与平台适配。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐