前言

在 OpenHarmony 应用开发中,使用 React Native(RN)进行跨平台开发已成为热门选择。表单处理作为应用开发中的常见需求,其复杂的状态管理和验证逻辑往往让开发者头疼。今天,我们将深入探讨如何在 OpenHarmony + RN 环境中自定义一个类似 Formik 的 useFormik Hook,实现优雅的表单处理方案。

一、为什么需要自定义表单处理?

1.1 现有方案的不足
原生 RN 表单:状态分散,验证逻辑混乱

第三方库兼容性:许多流行的 React 表单库在 OpenHarmony 环境中存在兼容性问题

性能考虑:轻量级解决方案更适合资源受限的移动设备

1.2 自定义 useFormik 的优势

// 目标 API 示例
const formik = useFormik({
  initialValues: { username: '', password: '' },
  validate: (values) => {/* 验证逻辑 */},
  onSubmit: (values) => {/* 提交处理 */}
});

二、核心设计与实现

2.1 基础 Hook 结构

// useFormik.ts
import { useState, useCallback, useRef } from 'react';

interface FormikConfig<T> {
  initialValues: T;
  validate?: (values: T) => Partial<Record<keyof T, string>>;
  onSubmit: (values: T) => void | Promise<void>;
  enableReinitialize?: boolean;
}

interface FormikResult<T> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  handleChange: (field: keyof T) => (value: any) => void;
  handleBlur: (field: keyof T) => () => void;
  handleSubmit: () => Promise<void>;
  setFieldValue: (field: keyof T, value: any) => void;
  resetForm: () => void;
  isValid: boolean;
  isSubmitting: boolean;
}

export function useFormik<T extends Record<string, any>>({
  initialValues,
  validate,
  onSubmit,
  enableReinitialize = false,
}: FormikConfig<T>): FormikResult<T> {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // 使用 ref 存储最新值,避免闭包问题
  const valuesRef = useRef(values);
  valuesRef.current = values;

2.2 验证系统实现

// 验证逻辑核心
const runValidation = useCallback(() => {
  if (!validate) return {};
  
  try {
    const validationErrors = validate(valuesRef.current);
    const newErrors: Partial<Record<keyof T, string>> = {};
    
    Object.keys(valuesRef.current).forEach(key => {
      const error = validationErrors[key as keyof T];
      if (error) {
        newErrors[key as keyof T] = error;
      }
    });
    
    return newErrors;
  } catch (error) {
    console.error('表单验证错误:', error);
    return {};
  }
}, [validate]);

// 实时验证效果
const validateField = useCallback((field: keyof T) => {
  if (!validate) return;
  
  const fieldError = validate(valuesRef.current)[field];
  setErrors(prev => ({
    ...prev,
    [field]: fieldError || undefined,
  }));
}, [validate]);

2.3 表单控件绑定器

// 为 OpenHarmony RN 控件创建适配器
export const createFormikField = <T,>(
  formik: FormikResult<T>,
  fieldName: keyof T
) => {
  return {
    value: formik.values[fieldName],
    onChange: (value: any) => {
      formik.setFieldValue(fieldName, value);
      formik.errors[fieldName] && formik.validateField?.(fieldName);
    },
    onBlur: () => formik.handleBlur(fieldName),
    error: formik.touched[fieldName] ? formik.errors[fieldName] : undefined,
  };
};

三、OpenHarmony 适配与优化

3.1 平台特定处理

// OpenHarmony 平台适配
import { Platform } from 'react-native';

const isOpenHarmony = Platform.OS === 'harmony';

// 针对 OpenHarmony 的事件处理优化
const createOpenHarmonyChangeHandler = <T,>(
  field: keyof T,
  setFieldValue: (field: keyof T, value: any) => void
) => {
  if (!isOpenHarmony) {
    return (value: any) => setFieldValue(field, value);
  }
  
  // OpenHarmony 特殊事件处理
  return (event: any) => {
    const value = event?.detail?.value ?? event?.nativeEvent?.value ?? event;
    setFieldValue(field, value);
  };
};

3.2 性能优化

// 防抖验证
const useDebouncedValidation = (
  values: T,
  validate: (values: T) => Partial<Record<keyof T, string>>,
  delay: number = 300
) => {
  const [debouncedErrors, setDebouncedErrors] = useState({});
  const timeoutRef = useRef<NodeJS.Timeout>();
  
  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    timeoutRef.current = setTimeout(() => {
      const newErrors = validate(values);
      setDebouncedErrors(newErrors);
    }, delay);
    
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [values, validate, delay]);
  
  return debouncedErrors;
};

四、完整使用示例

4.1 登录表单实现

// LoginForm.jsx
import React from 'react';
import { View, Text, Button, TextInput } from 'react-native';
import { useFormik, createFormikField } from './useFormik';

const LoginForm = () => {
  const formik = useFormik({
    initialValues: {
      email: '',
      password: '',
      rememberMe: false,
    },
    
    validate: (values) => {
      const errors = {};
      
      if (!values.email) {
        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位';
      }
      
      return errors;
    },
    
    onSubmit: async (values) => {
      // OpenHarmony 网络请求
      try {
        const response = await fetch('https://api.example.com/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(values),
        });
        
        if (response.ok) {
          console.log('登录成功');
        }
      } catch (error) {
        console.error('登录失败:', error);
      }
    },
  });
  
  const emailField = createFormikField(formik, 'email');
  const passwordField = createFormikField(formik, 'password');
  
  return (
    <View style={styles.container}>
      <TextInput
        style={[styles.input, emailField.error && styles.inputError]}
        placeholder="邮箱"
        value={emailField.value}
        onChangeText={emailField.onChange}
        onBlur={emailField.onBlur}
      />
      {emailField.error && (
        <Text style={styles.errorText}>{emailField.error}</Text>
      )}
      
      <TextInput
        style={[styles.input, passwordField.error && styles.inputError]}
        placeholder="密码"
        secureTextEntry
        value={passwordField.value}
        onChangeText={passwordField.onChange}
        onBlur={passwordField.onBlur}
      />
      {passwordField.error && (
        <Text style={styles.errorText}>{passwordField.error}</Text>
      )}
      
      <Button
        title="登录"
        onPress={formik.handleSubmit}
        disabled={!formik.isValid || formik.isSubmitting}
      />
      
      <Button
        title="重置"
        onPress={formik.resetForm}
        color="#999"
      />
    </View>
  );
};

const styles = {
  container: {
    padding: 20,
  },
  input: {
    height: 40,
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 4,
    paddingHorizontal: 10,
    marginBottom: 10,
  },
  inputError: {
    borderColor: 'red',
  },
  errorText: {
    color: 'red',
    fontSize: 12,
    marginBottom: 10,
  },
};

export default LoginForm;

4.2 复杂表单示例

// RegistrationForm.jsx
import React from 'react';
import { useFormik } from './useFormik';
import { 
  TextInput, 
  Switch, 
  Picker, 
  DatePicker,
  Button 
} from 'react-native';

const RegistrationForm = () => {
  const formik = useFormik({
    initialValues: {
      username: '',
      gender: 'male',
      birthDate: new Date(),
      agreeTerms: false,
      profile: {
        bio: '',
        website: '',
      },
    },
    
    validate: (values) => {
      const errors = {};
      
      // 嵌套对象验证
      if (values.profile.bio.length > 200) {
        errors['profile.bio'] = '个人简介不能超过200字';
      }
      
      return errors;
    },
    
    onSubmit: async (values) => {
      // 表单提交逻辑
    },
  });
  
  // 处理嵌套字段
  const handleNestedChange = (path: string, value: any) => {
    const parts = path.split('.');
    const newValues = { ...formik.values };
    let current: any = newValues;
    
    for (let i = 0; i < parts.length - 1; i++) {
      current = current[parts[i]];
    }
    
    current[parts[parts.length - 1]] = value;
    formik.setFieldValue(parts[0] as any, newValues[parts[0]]);
  };
  
  return (
    <View>
      {/* 各种表单控件 */}
    </View>
  );
};

五、高级特性扩展

5.1 表单数组支持

// 动态表单字段
interface UseFormikArray<T> {
  push: (value: T) => void;
  remove: (index: number) => void;
  swap: (indexA: number, indexB: number) => void;
  insert: (index: number, value: T) => void;
  fields: Array<{
    key: string;
    value: T;
    onChange: (value: T) => void;
    onRemove: () => void;
  }>;
}

export const useFormikArray = <T,>(
  fieldName: keyof T,
  formik: FormikResult<T>
): UseFormikArray<T[typeof fieldName]> => {
  // 实现动态数组操作
};

5.2 表单持久化

// OpenHarmony 数据持久化
import { Preferences } from '@ohos.data.preferences';

const useFormikPersist = <T,>(
  formik: FormikResult<T>,
  storageKey: string
) => {
  useEffect(() => {
    const loadFormData = async () => {
      try {
        const prefs = await Preferences.getPreferences(context, 'formData');
        const saved = await prefs.get(storageKey, '{}');
        const parsed = JSON.parse(saved);
        formik.setValues(parsed);
      } catch (error) {
        console.error('加载表单数据失败:', error);
      }
    };
    
    loadFormData();
  }, []);
  
  // 自动保存
  useEffect(() => {
    const saveFormData = async () => {
      try {
        const prefs = await Preferences.getPreferences(context, 'formData');
        await prefs.put(storageKey, JSON.stringify(formik.values));
        await prefs.flush();
      } catch (error) {
        console.error('保存表单数据失败:', error);
      }
    };
    
    const timeoutId = setTimeout(saveFormData, 500);
    return () => clearTimeout(timeoutId);
  }, [formik.values]);
};

六、测试策略

6.1 单元测试示例

// useFormik.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { useFormik } from './useFormik';

describe('useFormik', () => {
  it('应该初始化表单值', () => {
    const { result } = renderHook(() =>
      useFormik({
        initialValues: { name: '', age: 0 },
        onSubmit: jest.fn(),
      })
    );
    
    expect(result.current.values).toEqual({ name: '', age: 0 });
  });
  
  it('应该处理字段变更', () => {
    const { result } = renderHook(() =>
      useFormik({
        initialValues: { name: '' },
        onSubmit: jest.fn(),
      })
    );
    
    act(() => {
      result.current.setFieldValue('name', 'John');
    });
    
    expect(result.current.values.name).toBe('John');
  });
  
  it('应该执行验证', async () => {
    const validate = jest.fn(() => ({ name: '不能为空' }));
    
    const { result } = renderHook(() =>
      useFormik({
        initialValues: { name: '' },
        validate,
        onSubmit: jest.fn(),
      })
    );
    
    act(() => {
      result.current.setFieldValue('name', '');
    });
    
    expect(validate).toHaveBeenCalled();
    expect(result.current.errors.name).toBe('不能为空');
  });
});

6.2 OpenHarmony 环境测试

// 模拟 OpenHarmony 环境
const mockOpenHarmonyEnv = () => {
  jest.mock('react-native/Libraries/Utilities/Platform', () => ({
    OS: 'harmony',
    select: (obj: any) => obj.harmony,
  }));
};

七、总结与最佳实践

7.1 实现要点总结
类型安全:使用 TypeScript 泛型确保类型安全

性能优化:合理使用 useCallback、useMemo 避免不必要的重渲染

平台适配:考虑 OpenHarmony 平台特性

错误处理:完善的验证和错误反馈机制

可扩展性:设计灵活的 API 以支持各种业务场景

7.2 最佳实践建议
表单拆分:复杂表单拆分为多个子表单组件

自定义验证:根据业务需求扩展验证规则

防抖节流:高频操作添加防抖处理

内存管理:及时清理事件监听和定时器

无障碍支持:为表单控件添加适当的 accessibility 属性

7.3 未来扩展方向
表单联动:字段间的依赖和联动逻辑

可视化配置:通过 JSON Schema 生成表单

离线支持:增强的离线数据同步能力

插件系统:支持验证、提交等插件扩展

结语

通过本文的实现,我们成功在 OpenHarmony + RN 环境中创建了一个功能完整、性能优异的自定义 useFormik Hook。这个解决方案不仅解决了第三方库的兼容性问题,还针对 OpenHarmony 平台进行了专门优化。希望这个实践能为你带来启发,帮助你构建更优秀的 OpenHarmony 应用。

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐