没想到 React Hook Form 能在 React Native 里用的这么爽?

2,480 阅读5分钟

logo.png

在我最近开发的React Native项目中,需要用到一个特别常见的功能:表单校验。我需要确保用户在提交表单之前输入了有效的数据,并提供适当的错误提示。在寻找解决方案时,我发现了 React Hook Form。它是一个轻量级且易于使用的表单库,它不仅提供了强大的表单校验功能,还具有良好的扩展性,可以很方便的集成第三方组件。

基本使用

在您的 React Native 项目中,添加以下 React Hook Form 的 React Native 示例代码(react-hook-form.com/get-started…

// App.tsx
import { Text, View, TextInput, Button, Alert } from "react-native"
import { useForm, Controller } from "react-hook-form"


export default function App() {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {
      firstName: "",
      lastName: "",
    },
  })
  const onSubmit = (data) => console.log(data)


  return (
    <View>
      <Controller
        control={control}
        rules={{
          required: true,
        }}
        render={({ field: { onChange, onBlur, value } }) => (
          <TextInput
            placeholder="First name"
            onBlur={onBlur}
            onChangeText={onChange}
            value={value}
          />
        )}
        name="firstName"
      />
      {errors.firstName && <Text>This is required.</Text>}


      <Controller
        control={control}
        rules={{
          maxLength: 100,
        }}
        render={({ field: { onChange, onBlur, value } }) => (
          <TextInput
            placeholder="Last name"
            onBlur={onBlur}
            onChangeText={onChange}
            value={value}
          />
        )}
        name="lastName"
      />


      <Button title="Submit" onPress={handleSubmit(onSubmit)} />
    </View>
  )
}

在示例代码中,从 react-hook-form 中导出了 useForm 函数 和 Controller 组件,通过调用 userForm 函数来初始化表单的相关状态和方法。

control 是在调用 useForm 函数时初始化的表单控制器对象,control 中管理当前初始化表单的核心API,通过这个 control 对象,我们可以非常方便的管理整个表单的状态。

Controller 就是一个包装器组件,用于管理某个字段的输入和输出。它的工作原理是通过传入的 namecontrol 中获取指定字段相关的属性和方法,并且进行了一层包装,使其更加易于使用,最后通过回调的方式将包装好的属性和方法传递给了 render 属性,然后就可以在 render 的函数实参中获取到这些属性和方法。所以通过 Controller 组件,可以使我们很方便的集成第三方组件。

Controller 组件还可以传入校验规则 rules 字段,支持必填项,最大值,最小值,正则,自定义校验函数等规则。

更多参数细节请查阅官网:Controller 组件.

在上方实例中,从 useForm 函数还解构出来另外两个成员,formState 对象 和 handleSubmit 函数,

formState 对象也是在 useForm 函数调用时初始化的,它其实是要比 control 对象初始化的还要早。上面我们讲了 control 对象的作用就是管理着表单各种核心API也管理着表单的状态,而 formState 对象的作用就是用来记录表单的状态,例如当前表单的字段是否被修改过,是否校验过,是否提交过,以及校验失败的信息(校验的类型,自定义的错误提示)等。正因为有了这些状态的记录,使得我们在碰到一些特殊业务场景可以很轻松的实现。

最后一个就是 handleSubmit 函数,它接收两个函数参数,后者非必填。当调用此函数,会触发表单校验,当校验通过后将会回调第一个参数参数,并且将所有表单字段values传递给该函数。当校验失败则会回调第二个函数参数,并且从上面提到过的 formState 中拿到 errors (校验失败的信息),传递给第二个函数。

 const onSubmit = (data) => { console.log('校验通过', data) }
 const onError = (errors) => { console.log('校验失败', errors) }

 <Button title="Submit" onPress={handleSubmit(onSubmit, onError)} />

以上就是 React Hook Form 在 React Native 中的基础使用。

虽然 React Hook Form 已经非常强大了,但我觉得实际业务搬砖中,目前这样子写起来还是比较繁琐,例如错误信息,我需要关心它是否显示错误信息,并且还要管理它的样式,有10个需要校验的表单字段,我就需要把代码拷贝10次,虽然当时拷贝起来比较省心,但是后期维护起来其实是很不方便的。

因为在实际的业务中表单字段的错误样式 UI / UX 基本是相同的,如果不相同那就干UI(狗头保命)。从长远的摸鱼之计,提高代码的可维护性是非常重要的,毕竟 Fix bug / Change request 的时长决定了我们摸鱼的时长。

接下来会基于 React Hook Form 封装一个 FormItem 组件,该组件内部预设好统一的错误提示,统一的 label 样式,这样我们在开发时只需要关心 render 的逻辑即可。

封装 FormItem

效果图:

Kapture 2023-10-27 at 22.59.02.gif

// App.tsx
import {
  Text,
  View,
  TextInput,
  Button,
  Alert,
  TextInputProps,
  StyleSheet
} from 'react-native'
import { useForm } from 'react-hook-form'

import FormItem from './src/components/FormItem'

const emailRegEx =
  /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

const Input = (props: TextInputProps) => {
  return (
    <TextInput
      {...props}
      style={{
        fontSize: 20,
        height: 40,
        paddingLeft: 10
      }}
    />
  )
}

export default function App() {
  const {
    control,
    handleSubmit,
    formState: { errors }
  } = useForm({
    defaultValues: {
      email: '',
      password: ''
    }
  })

  const onSubmit = () => {
    Alert.alert('提交成功~💐')
  }

  return (
    <View style={styles.wrapper}>
      <Text style={styles.title}>Sign in</Text>

      <FormItem
        required
        name="email"
        label="Email"
        control={control}
        errors={errors.email}
        rules={{
          required: '请输入邮箱',
          pattern: {
            value: emailRegEx,
            message: '请输入一个有效的邮箱'
          }
        }}
        render={({ field: { onChange, value } }) => (
          <Input
            value={value}
            onChangeText={onChange}
            placeholder="请输入邮箱"
          />
        )}
        style={{ marginBottom: 40 }}
      />

      <FormItem
        required
        label="Password"
        control={control}
        name="password"
        rules={{
          required: '请输入密码'
        }}
        errors={errors.password}
        render={({ field: { onChange, value } }) => (
          <Input
            value={value}
            onChangeText={onChange}
            placeholder="请输入密码"
            secureTextEntry
          />
        )}
      />

      <Button title="Sign in" onPress={handleSubmit(onSubmit)} />
    </View>
  )
}

const styles = StyleSheet.create({
  wrapper: {
    flex: 1,
    justifyContent: 'center',
    paddingLeft: 15,
    paddingRight: 15
  },
  title: {
    marginBottom: 30,
    fontSize: 50
  }
})

// src/components/FormItem/index.tsx
import React from 'react'
import {
  Controller,
  ControllerProps,
  FieldValues,
  UseControllerProps,
  GlobalError,
  FieldPath
} from 'react-hook-form'

import { TextStyle, View, Text, ViewStyle } from 'react-native'

type FormItemProps<T extends FieldValues, TName extends FieldPath<T>> = {
  label?: string
  required?: boolean
  errors?: GlobalError
  style?: ViewStyle
  labelStyle?: TextStyle
  border?: boolean
} & ControllerProps<T, TName> &
  UseControllerProps<T, TName>

const FormItem = <T extends FieldValues, TName extends FieldPath<T>>(
  props: FormItemProps<T, TName>
) => {
  const {
    name,
    control,
    rules,
    label,
    required,
    errors,
    style = {},
    labelStyle = {},
    border = true,
    render
  } = props

  return (
    <View key={name} style={style}>
      {label && (
        <View
          style={{
            flexDirection: 'row',
            alignItems: 'center'
          }}
        >
          <Text
            style={{
              fontSize: 20,
              marginBottom: 5,
              fontWeight: '700',
              ...labelStyle
            }}
          >
            {label}
          </Text>
          {required && (
            <Text style={{ marginLeft: 4, color: 'red', fontSize: 20 }}>*</Text>
          )}
        </View>
      )}

      <View
        style={{
          borderWidth: 1,
          ...(!errors
            ? {
                borderColor: border ? '#B3BAC1' : 'transparent'
              }
            : {
                borderColor: border ? '#D52D0B' : 'transparent'
              })
        }}
      >
        <Controller
          name={name}
          control={control}
          rules={rules}
          render={render}
        />
      </View>
      {rules && errors && errors?.message && (
        <View
          style={{
            marginTop: 4
          }}
        >
          <Text
            style={{
              color: 'red'
            }}
          >
            {errors?.message}
          </Text>
        </View>
      )}
    </View>
  )
}

export default FormItem

以上就是 FormItem 组件及示例代码并且包含 Typescript 的类型,复制到您的项目中就可以运行,然后您可以根据您的具体业务和 UI / UX 进行调整。

FormItem 就是一个非常简单的封装,核心组件还是我们之前提到过的 Controller 包装器组件,只不过是多了一些通用的自定义元素,例如我这里结合自己的需求扩展了 label 和 error,这样就大大提高了代码的可维护性,例如 UI/UX 修改了表单校验的样式,这样我就可以直接修改 FormItem 组件即可。

END 🏄🏻

感谢阅读 :P