React hooks 和 Antd Form

7,089 阅读3分钟

问题描述

在一个 Function 组件中,使用 Ant Design 的组件 Form,实现搜索关键字的功能。主要代码如下:

import React, { useEffect, useState } from "react"
import { Form, Input } from "antd";

function Demo() {
  const [ keyword, setKeyword ] = useState('')	// 声明一个state变量,存搜索框输入的内容
  
  useEffect(getData, [keyword])	// 更新数据,依赖于 keyword
  
  function getData() {
    let params = {
      query: keyword
    }
    // 把 params 作为参数,发送请求获取搜索结果
    // ...
  }
  
  function onChangeKeyword(e){
    setKeyword(e.target.value)
  }
  
  return (
    <Form onFinish={getData}>
      <Form.Item name="keyword">
         <Input.Search
            value={keyword}
            placeholder="输入关键字搜索"
            onChange={onChangeKeyword}
            onPressEnter={getData}
            allowClear
        />
      </Form.Item>
    </Form>   
  )
}

以上代码有几个问题:

  1. 每一次输入框的内容改变,getData 就会被调用,于是频繁发送请求。正常是希望输入完成之后,点击“搜索”按钮或者输入回车再发送搜索请求。getData 中使用了 keyword,所以 useEffect 的依赖列表中必须有 keyword。怎么避免发送不必要的请求呢?
  2. Form 用法有问题。直接摘取 Antd 文档中 Form 组件的 API 介绍:

    被设置了 name 属性的 Form.Item 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:

    • 你不再需要也不应该用 onChange 来做数据收集同步(你可以使用 Form 的 onValuesChange),但还是可以继续监听 onChange 事件。
    • 你不能用控件的 value 或 defaultValue 等属性来设置表单域的值,默认值可以用 Form 里的 initialValues 来设置。注意 initialValues 不能被 setState 动态更新,你需要用 setFieldsValue 来更新。
    • 你不应该用 setState,可以使用 form.setFieldsValue 来动态改变表单值。

解决办法

  1. Input 组件去掉 onChange 监听和 value 属性。
    <Input.Search
        placeholder="输入关键字搜索"
        onPressEnter={getData}
        allowClear
    />
    
  2. 去掉 keyword state。
  3. 通过 Form.useForm 对表单数据域进行交互。调用 getFieldValue 获取输入框的内容。(如果是 class 组件,可以使用 ref)。
    const [form] = Form.useForm();
    
    useEffect(getData, [form])	// 依赖于 form,输入框变化时 form 不变
    
      function getData() {
        let params = {
          query: form.getFieldValue('keyword')
        }
        // ...
      }
    
    
    这样的话,搜索框内容的变化就不会触发 getData了。

思考

如果这里用的是 input 标签呢?也就是说优化前的代码是这样的:

function Demo() {
  const [ keyword, setKeyword ] = useState('')	// 声明一个state变量,存搜索框输入的内容
  
  useEffect(getData, [keyword])	// 更新数据,依赖于 keyword
  
  function getData() {
    let params = {
      query: keyword
    }
    // ...
  }
  
  function onChangeKeyword(e){
    setKeyword(e.target.value)
  }
  
  return (
  <input value={keyword} onChange={onChangeKeyword} type="text"/>
  )
}

优化办法:

思路是一样的,使用 ref。

  1. 把受控的 input 改成非受控的,即去掉 value 属性和 onchange 监听。
  2. 去掉 keyword state。
  3. 使用 useRef 创建input 元素的引用,然后就可以用inputRef.current.value 获取输入框的内容。
  const inputRef = useRef(null)
  
  useEffect(getData, [])	// ref 不需要放到依赖数组里
  
  function getData() {
    let params = {
      query: inputRef.current.value
    }
    // ...
  }
  
  return (
  <input ref={inputRef} type="text"/>
  )
}

一点疑惑

至于为什么 inputRef.current 不需要放到 useEffect 的依赖里面呢?

Dan Abramov 在这个issue里面解释:

...Dependencies should only include values that participate in top-down React data flow. Such as props, state, and what you calculate from them. Ref containers are fine too (since they can be passed down).

However, it doesn’t make sense to add ref.current as a dependency. For the same reason it doesn’t make sense to add window.myVariable. When it updates, React won’t know about it, and won’t update your component.

还有这个issue里面说到:

If you want to use state, just use state directly. Refs are for mutable values that are intended to be mutated outside the render data flow. If you want them to cause a re-render, you shouldn't be using a ref.

I'm going to close this thread. As I mentioned earlier, if you put [ref.current] in dependencies, you're likely making a mistake. Refs are for values whose changes don't need to trigger a re-render.

综上大概是说,useEffect 只应该依赖参与 React 自上而下数据流的变量,比如 props,state,或者由它们计算出来的值。而 Ref 是用于在渲染数据流外改变的值,当 refs 发生变化,react 并不会知道,当然也就并不会触发重新渲染。