MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 表单处理与自定义 Hooks 的实现

2022-09-093.6k 阅读

React 表单处理基础

在 React 应用中,表单是收集用户输入数据的重要组件。React 提供了一种独特的方式来处理表单,使得表单数据的管理和交互更加高效和可预测。

受控组件

React 中,表单元素(如 <input><textarea><select> 等)通常是受控组件。所谓受控组件,就是其值由 React 组件的 state 控制。例如,对于一个简单的文本输入框:

import React, { useState } from 'react';

function ControlledInput() {
  const [inputValue, setInputValue] = useState('');

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
      />
      <p>输入的值是: {inputValue}</p>
    </div>
  );
}

export default ControlledInput;

在上述代码中,inputValue 存储在组件的 state 中,输入框的 value 属性绑定到这个 state 值。当输入框内容发生变化时,handleChange 函数会被调用,更新 inputValue,从而实现对输入框值的实时控制。

对于单选框和复选框,同样可以通过受控组件的方式处理。以单选框为例:

import React, { useState } from 'react';

function RadioButtons() {
  const [selectedOption, setSelectedOption] = useState('option1');

  const handleChange = (e) => {
    setSelectedOption(e.target.value);
  };

  return (
    <div>
      <input
        type="radio"
        id="option1"
        value="option1"
        checked={selectedOption === 'option1'}
        onChange={handleChange}
      />
      <label for="option1">选项 1</label>
      <input
        type="radio"
        id="option2"
        value="option2"
        checked={selectedOption === 'option2'}
        onChange={handleChange}
      />
      <label for="option2">选项 2</label>
      <p>选中的选项是: {selectedOption}</p>
    </div>
  );
}

export default RadioButtons;

这里,selectedOption 控制着单选框的选中状态,checked 属性根据 selectedOption 的值来决定哪个单选框被选中。

非受控组件

与受控组件相对的是非受控组件,非受控组件的数据由 DOM 本身来管理。在 React 中,可以使用 ref 来访问非受控组件的值。例如:

import React, { useRef } from 'react';

function UncontrolledInput() {
  const inputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('输入的值是: ', inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default UncontrolledInput;

在这个例子中,inputRef 是一个 ref 对象,通过 ref 属性绑定到输入框。当表单提交时,通过 inputRef.current.value 可以获取到输入框的值。非受控组件在某些场景下很有用,比如文件上传组件,因为它们的 value 属性是只读的,无法通过受控方式管理。

React 表单验证

表单验证是确保用户输入数据有效性的重要环节。在 React 中,可以在受控组件的 onChange 事件或者表单提交时进行验证。

简单的文本输入验证

以一个要求输入长度至少为 5 的文本输入框为例:

import React, { useState } from 'react';

function TextInputValidation() {
  const [inputValue, setInputValue] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const value = e.target.value;
    setInputValue(value);
    if (value.length < 5) {
      setError('输入长度至少为 5');
    } else {
      setError('');
    }
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
      />
      {error && <p style={{ color:'red' }}>{error}</p>}
    </div>
  );
}

export default TextInputValidation;

handleChange 函数中,当输入值长度小于 5 时,设置错误信息并显示在页面上。

复杂表单验证

对于包含多个字段的复杂表单,可以将验证逻辑封装成函数。例如,一个包含用户名和密码的登录表单:

import React, { useState } from 'react';

const validateForm = (username, password) => {
  let errors = {};
  if (!username) {
    errors.username = '用户名不能为空';
  }
  if (!password) {
    errors.password = '密码不能为空';
  } else if (password.length < 6) {
    errors.password = '密码长度至少为 6';
  }
  return errors;
};

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const handleSubmit = (e) => {
    e.preventDefault();
    const formErrors = validateForm(username, password);
    setErrors(formErrors);
    if (Object.keys(formErrors).length === 0) {
      // 处理表单提交逻辑
      console.log('表单提交成功');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
        {errors.username && <span style={{ color:'red' }}>{errors.username}</span>}
      </label>
      <label>
        密码:
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        {errors.password && <span style={{ color:'red' }}>{errors.password}</span>}
      </label>
      <button type="submit">登录</button>
    </form>
  );
}

export default LoginForm;

在这个例子中,validateForm 函数接收用户名和密码作为参数,返回一个包含错误信息的对象。在 handleSubmit 函数中,调用 validateForm 进行验证,并根据验证结果显示错误信息或处理表单提交。

自定义 Hooks 基础

Hooks 是 React 16.8 引入的新特性,它允许在不编写类的情况下使用 state 和其他 React 特性。自定义 Hooks 则是一种将组件逻辑提取到可复用函数中的方式。

自定义 Hooks 的创建

自定义 Hooks 本质上是一个 JavaScript 函数,其名称以 use 开头。例如,创建一个简单的 useToggle Hooks 来切换布尔值:

import { useState } from'react';

const useToggle = (initialValue = false) => {
  const [value, setValue] = useState(initialValue);
  const toggle = () => {
    setValue(!value);
  };
  return [value, toggle];
};

function ToggleComponent() {
  const [isOn, toggle] = useToggle();

  return (
    <div>
      <button onClick={toggle}>{isOn? '关闭' : '打开'}</button>
    </div>
  );
}

export default ToggleComponent;

在上述代码中,useToggle 是一个自定义 Hooks,它返回一个包含当前值和切换函数的数组。ToggleComponent 组件使用这个自定义 Hooks 来实现一个简单的开关功能。

自定义 Hooks 的作用域

自定义 Hooks 的作用域与调用它的组件相关。每个组件调用自定义 Hooks 时,都会有自己独立的 state 和副作用。例如:

import { useState } from'react';

const useCounter = () => {
  const [count, setCount] = useState(0);
  const increment = () => {
    setCount(count + 1);
  };
  return [count, increment];
};

function CounterComponent1() {
  const [count, increment] = useCounter();

  return (
    <div>
      <p>组件 1 的计数: {count}</p>
      <button onClick={increment}>增加</button>
    </div>
  );
}

function CounterComponent2() {
  const [count, increment] = useCounter();

  return (
    <div>
      <p>组件 2 的计数: {count}</p>
      <button onClick={increment}>增加</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <CounterComponent1 />
      <CounterComponent2 />
    </div>
  );
}

export default App;

在这个例子中,CounterComponent1CounterComponent2 都使用了 useCounter 自定义 Hooks,但它们的 count state 是相互独立的,点击各自的增加按钮只会影响本组件内的计数。

使用自定义 Hooks 处理表单

将自定义 Hooks 应用到表单处理中,可以使表单逻辑更加清晰和可复用。

通用表单状态管理 Hooks

创建一个用于管理表单状态的自定义 Hooks useFormState

import { useState } from'react';

const useFormState = (initialState = {}) => {
  const [formState, setFormState] = useState(initialState);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormState({
     ...formState,
      [name]: value
    });
  };

  return [formState, handleChange];
};

function FormComponent() {
  const [formData, handleChange] = useFormState({
    username: '',
    password: ''
  });

  return (
    <form>
      <label>
        用户名:
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
      </label>
      <label>
        密码:
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
      </label>
    </form>
  );
}

export default FormComponent;

useFormState Hooks 接收一个初始状态对象,返回当前表单状态和一个处理输入框变化的函数。FormComponent 组件使用这个 Hooks 来管理表单数据,简化了每个输入框的状态管理代码。

表单验证自定义 Hooks

结合前面的表单验证逻辑,可以创建一个用于表单验证的自定义 Hooks useFormValidation

import { useState } from'react';

const validateForm = (formData) => {
  let errors = {};
  if (!formData.username) {
    errors.username = '用户名不能为空';
  }
  if (!formData.password) {
    errors.password = '密码不能为空';
  } else if (formData.password.length < 6) {
    errors.password = '密码长度至少为 6';
  }
  return errors;
};

const useFormValidation = () => {
  const [errors, setErrors] = useState({});

  const validate = (formData) => {
    const formErrors = validateForm(formData);
    setErrors(formErrors);
    return Object.keys(formErrors).length === 0;
  };

  return [errors, validate];
};

function ValidationFormComponent() {
  const [formData, handleChange] = useFormState({
    username: '',
    password: ''
  });
  const [errors, validate] = useFormValidation();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate(formData)) {
      // 处理表单提交逻辑
      console.log('表单提交成功');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
        {errors.username && <span style={{ color:'red' }}>{errors.username}</span>}
      </label>
      <label>
        密码:
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span style={{ color:'red' }}>{errors.password}</span>}
      </label>
      <button type="submit">提交</button>
    </form>
  );
}

export default ValidationFormComponent;

useFormValidation Hooks 负责管理表单验证的错误状态,并提供一个验证函数。ValidationFormComponent 组件结合 useFormStateuseFormValidation 来实现一个完整的带验证功能的表单。

自定义 Hooks 的高级应用

除了基本的表单处理,自定义 Hooks 还可以在更复杂的场景中发挥作用。

结合副作用的自定义 Hooks

有时候,我们需要在表单状态变化时执行一些副作用操作,比如发送网络请求。可以创建一个结合 useEffect 的自定义 Hooks。例如,当用户名输入变化时,检查用户名是否已存在:

import { useState, useEffect } from'react';

const useUsernameCheck = () => {
  const [username, setUsername] = useState('');
  const [isAvailable, setIsAvailable] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (username.length > 0) {
      setLoading(true);
      // 模拟网络请求
      setTimeout(() => {
        const available = username!== 'existingUser';
        setIsAvailable(available);
        setLoading(false);
      }, 1000);
    } else {
      setIsAvailable(null);
      setLoading(false);
    }
  }, [username]);

  return { username, setUsername, isAvailable, loading };
};

function UsernameCheckComponent() {
  const { username, setUsername, isAvailable, loading } = useUsernameCheck();

  return (
    <div>
      <label>
        用户名:
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
      </label>
      {loading && <p>检查中...</p>}
      {isAvailable!== null && (
        <p>{isAvailable? '用户名可用' : '用户名已存在'}</p>
      )}
    </div>
  );
}

export default UsernameCheckComponent;

useUsernameCheck Hooks 中,通过 useEffect 依赖 username 的变化。当 username 有值时,模拟一个网络请求来检查用户名是否可用,并更新 isAvailableloading 状态。UsernameCheckComponent 组件使用这个 Hooks 来实现用户名实时检查功能。

自定义 Hooks 的组合

自定义 Hooks 可以相互组合,以构建更复杂的功能。例如,将前面的 useFormStateuseFormValidationuseUsernameCheck 组合起来:

function CombinedFormComponent() {
  const [formData, handleChange] = useFormState({
    username: '',
    password: ''
  });
  const [errors, validate] = useFormValidation();
  const { username, setUsername, isAvailable, loading } = useUsernameCheck();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate(formData) && isAvailable) {
      // 处理表单提交逻辑
      console.log('表单提交成功');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={(e) => {
            handleChange(e);
            setUsername(e.target.value);
          }}
        />
        {loading && <p>检查中...</p>}
        {isAvailable!== null && (
          <p>{isAvailable? '用户名可用' : '用户名已存在'}</p>
        )}
        {errors.username && <span style={{ color:'red' }}>{errors.username}</span>}
      </label>
      <label>
        密码:
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span style={{ color:'red' }}>{errors.password}</span>}
      </label>
      <button type="submit">提交</button>
    </form>
  );
}

export default CombinedFormComponent;

CombinedFormComponent 中,通过组合多个自定义 Hooks,实现了一个包含表单状态管理、验证以及用户名可用性检查的复杂表单。这种组合方式使得代码逻辑更加模块化和可维护。

通过以上对 React 表单处理和自定义 Hooks 的深入探讨,我们可以看到如何利用这些技术构建高效、可复用且易于维护的前端表单组件。无论是简单的文本输入还是复杂的多字段表单,通过合理运用受控组件、非受控组件、表单验证以及自定义 Hooks,都能够实现出色的用户体验和业务逻辑。在实际项目中,根据具体需求灵活选择和组合这些技术,将有助于提升开发效率和代码质量。