react hooks是 react 16.8 引入的特性,这里我们通过对react-hook-form进行分析来了解成熟的库是如何使用hook的。这将是一个系列,首先推荐 useRef
在react中,我们使用Ref
来获取组件的实例或者DOM元素。我们可以使用两种方式来创建 Ref
import * as React from 'react' import { useState, useEffect, useRef, createRef } from 'react' export default () => { const ref1 = createRef<HTMLFormElement>() const ref2 = useRef<HTMLInputElement>() useEffect(() => { console.log(ref1) console.log(ref2) }, []) return ( <form ref={ref1}> <label>用户信息</label> <input type="text" ref={ref2} /> </form> ) }
上面两种方式都能在组件 mounted 之后获取相关的DOM元素。在一个组件的正常的生命周期中可以大致可以分为3个阶段
在第1阶段,使用createRef
与useRef
两者是没有区别。但是在第2阶段, 也就是更新阶段两者是有区别的。我们知道,在一个局部函数中,函数每一次执行,都会在把函数的变量重新生成一次。
export default () => { const ref1 = createRef<HTMLFormElement>() const ref2 = useRef<HTMLInputElement>() const [ count, setCount ] = useState<number>(0) useEffect(() => { if (!store.ref1) { store.ref1 = ref1 } else { console.log(store.ref1 === ref1) } }) useEffect(() => { setTimeout(() => { setCount(1) }, 1000) }, []) return ( <form ref={ref1}> <label>用户信息</label> <input type="text" ref={ref2} /> </form> ) }
我们使用一个外部的变量store
来存储初次所创建的ref,在我们对组件进行更新后,会发现更新后的ref与我们初次创建的ref其实并不一致。这样也就意味着我们每更新一次组件, 就重新创建一次ref。
由于有上面的问题,这在函数组件中,使用createRef去获取ref是不合理的。所以hook给我们提供一个新的API, 就是useRef
。在useRef
创建的ref仿佛就像外部定义的一个全局变量,不会随着组件的更新而重新创建。但组件销毁,它也会消失,不用手动进行销毁。
export default () => { const ref1 = createRef<HTMLFormElement>() const ref2 = useRef<HTMLInputElement>() const [ count, setCount ] = useState<number>(0) useEffect(() => { if (!store.ref1) { store.ref1 = ref1 } else { console.log('ref1:', store.ref1 === ref1) } if (!store.ref2) { store.ref2 = ref2 } else { console.log('ref2:', store.ref2 === ref2) } }) useEffect(() => { setTimeout(() => { setCount(1) }, 1000) }, []) return ( <form ref={ref1}> <label>用户信息</label> <input type="text" ref={ref2} /> </form> ) }
通过上面的说明,我们知道useRef创建的ref并不会随着组件的更新而重新构建。由于这个特性,在使用react-hook的时候,可以使用useRef来存储常量。
现在回到我们的主题,看看react-hook-form是如何处理ref。在react-hook-form有一个API为register。
源码实现如下
... function register(refOrValidationOptions, validationOptions) { if (isWindowUndefined) { return; } if (isString(refOrValidationOptions)) { registerFieldsRef({ name: refOrValidationOptions }, validationOptions); return; } if (isObject(refOrValidationOptions) && 'name' in refOrValidationOptions) { registerFieldsRef(refOrValidationOptions, validationOptions); return; } return (ref) => ref && registerFieldsRef(ref, refOrValidationOptions); }
使用闭包存储了对当前输入框的validationOptions
, 返回的函数被ref的接收。这里使用了ref另外一种获取方式‘回调refs’
import { useForm } from 'react-hook-form' export default () => { const { register, errors, handleSubmit } = useForm() const submit = useCallback((data, e) => { console.log(data, e) }, []) useEffect(() => { console.log(errors) }) return ( <form onSubmit={handleSubmit(submit)}> <label>用户信息</label> <input name="userName" ref={register({ required: true })} /> {errors.userName && "Your input is required"} <button type={'submit'}>提交</button> </form> ) }
这里为什么使用回调refs
,而不是refs
。其实理由很简单,因为后面要引用的DOM元素或者React实例是未知,我们是不知道使用者会把register注册到INPUT、TEXTAREA、还是其他的第三方组件。注册一个还是多个。使用回调refs
我们能够直接获取到对应的真实DOM元素或者React实例,而使用了refs
就会失去这种灵活性。
如果我们继续往后面进行分析,会看到useForm这个hook中,使用了大量的useRef来存储变量,原因看前面。
而前面通过register中调用的ref对象被注册到filedsRef中。