让组件管理自己的状态?这句话听起来似乎是一件理所当然的事情,但是让我们看以下例子:
// MyPage.tsx
const MyPage: React.FC = () => {
const [modalVisible, setModalVisible] = setState<boolean>(false)
const toggleModal = () => {
setModalVisible(!modalVisible)
}
return (
<div>
<Button onClick={toggleModal}>点击</Button>
<MyModal visible={modalVisible} />
</div>
)
}
// MyModal.tsx
consy MyModal: React.React.FC = ({visible}) => {
return <Modal visible={visible}>
<!-- JSX.Element -->
</Modal>
}
export default MyModal
为了方便理解,我们称 MyPage
为页面组件,Button
和 MyModal
等为基础/业务组件,一个页面由多个基础/业务组件构成。
上面代码是一个很简单的例子:MyPage
包含 Button
和 MyModal
两个组件,并且拥有 modalVisible
这个状态来控制 MyModal
的可见状态。
上面的代码在功能上完全没有问题,但是本文的观点是既然 visible
是 MyModal
组件自身的状态,那就应该让 MyModal
自身去维护,而不是在 MyPage
里面设置一个状态去进行管理。
如果你同意上述观点,就可以继续往下面看了,不然的话可以在评论区留下你的观点,或者点个赞再离开😁。
useImperativeHandle & forwardRef
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。
国际惯例,先上代码:
// MyPage.tsx
const MyPage: React.FC = () => {
const modalRef = useRef(null);
const toggleModal = () => {
modalRef.current.toggle()
}
return (
<div>
<Button onClick={toggleModal}>点击</Button>
<MyModal ref={modalRef} />
</div>
)
}
// MyModal.tsx
consy MyModal: React.ForwardRefRenderFunction = (props, ref) => {
const [visible, setVisible] = useState<boolean>(false)
useImperativeHandle(ref, ({
toggle: () => {
setVisible(!visible)
}
}))
return <Modal visible={visible}>
<!-- JSX.Element -->
</Modal>
}
export default React.forwardRef(MyModal)
forwardRef
有两个作用,分别是将 ref
从父组件转发到子组件的 DOM 元素上以及让子组件接受 props
和 ref
作为参数。
useImperativeHandle
useImperativeHandle
限制了暴露给父组件调用的方法,比如上述例子,MyPage
只能调用 MyModal
的 toggle 方法。这使得父组件可以不用关注子组件的内部实现,而只需要关注简单的几个方法的作用;同时,子组件状态在内部闭环,符合组件开发的开闭原则。
总结
与 useEffect
和 useCallback
等 API 相比,useImperativeHandle
(通常和 forwardRef
一起使用!!!) 这个 Hook API 可能收到的关注比较少,但是它在一些公用组件开发中却起到非常大的作用:组件管理自身状态以及公开必要的方法给到外部调用。
本文只是从一个简单的例子进行介绍,还有很多不全面的地方,欢迎大家指正。更详细的 API 介绍大家可以查看官方文档,里面有一个 input 相关的更简洁的例子,可以帮助大家快速理解 API 的使用和作用。