在typescript中使用useImperativeHandle 传递 ref
现在有一个Button
组件, 组件中包含一个按钮和一个Modal, 同时这个Modal中引用了另外一个Form
组件, 见代码
const AddButton: React.FC = () => {
const [showAddModal, setShowAddModal] = useState(false);
return (
<div>
<Button onClick={() => setShowAddModal(true)}>
新增用户
</Button>
<Modal
open={showAddModal}
onCancel={() => setShowAddModal(false)}
onOk={() => {}}
>
<UserForm/>
</Modal>
</div>
);
};
UserForm
组件
import { Input,Form } from 'antd';
const UserForm:React.FC = (() => {
return (
<div>
<Form name="userForm">
<Form.Item name="username" label="账号" >
<Input />
</Form.Item>
<Form.Item name="password" label="密码">
<Input.Password />
</Form.Item>
{/* ... 省略*/}
</Form>
</div>
);
});
export default UserForm;
上面只是两个最基础的组件, 如果现在我想在Modal
的onOk
事件中处理表单数据, 可以选择的方法也很多, 这里只说传递ref
的方法
首先开始改造 UserForm
组件, 将组件通过forwardRef
进行包裹, 再通过antd
提供的useForm()
方法获取form
实例, 最后通过useImperativeHandle
方法将ref传递
开始改造, 上代码
import { Input, Form, FormInstance } from 'antd';
import { forwardRef, useImperativeHandle } from 'react';
// 定义一个ref的类型, 在其他组件使用时就获得这里面的提示
// FormInstance就是 Antd的 form类型
export type IRefUserForm = { form: FormInstance};
// 组件 props, 先占位
interface IProps {
}
// 改造组件, 抛弃 React.FC, 而使用 forwardRef 包裹
// forwardRef 会提供2个参数, 注意是参数不是泛型参数, 第一个就是我们的props, 第二个就是要处理的 ref
// forwardRef 接受2个泛型参数
// 1. 第一个泛型参数为要暴露出去的ref类型
// 2. 第二个泛型参数为 props 的类型
const UserForm = forwardRef<IRefUserForm, IProps>((props, ref) => {
// 获取一个 Form的ref, 在下面 <Form form={form}>中绑定
const [form] = Form.useForm();
// 第一个参数为接收到的ref,
// 第二个参数是一个初始化函数, 返回值将会变成 ref的值
// 第三个参数是一个依赖数组, 当依赖项改变时, 会重新执行第二个参数的函数
useImperativeHandle(
ref,
() => ({
form: form,
}),
[form]
);
return (
<div>
<Form name="userForm"
form={form}
>
<Form.Item name="username" label="账号" >
<Input />
</Form.Item>
<Form.Item name="password" label="密码">
<Input.Password />
</Form.Item>
{/* ... 省略*/}
</Form>
</div>
);
});
export default UserForm;
接下来, 让我们一起来改造一下 AddButton
的组件吧
首先要使用useRef
创造一个ref
, 然后再将这个ref
传入到UserForm
中
最后, 修改onOk
事件, 使用上面的ref
来操作form
实例即可
const AddButton: React.FC = () => {
import { Button, Modal } from 'antd';
import { useRef, useState } from 'react';
// 引用了 IRefUserForm 这个类型
import UserForm, { IRefUserForm } from './UserForm';
const [showAddModal, setShowAddModal] = useState(false);
// 这里在使用useRef时我们指定了他的类型, 并且使用 null类初始化
// 记得一定要用null来初始化, 不然会类型错误, 看一下useRef的定义就知道了
// function useRef<T>(initialValue: T|null): RefObject<T>;
// 上面是useRef的类型定义, 必须使用 传入的泛型或null来赋值, 很明显, 我们没有 IRefUserForm 类型的值,所以只能用 null了
const formRef = useRef<IRefUserForm>(null);
return (
<div>
<Button onClick={() => setShowAddModal(true)}>
新增用户
</Button>
<Modal
open={showAddModal}
onCancel={() => setShowAddModal(false)}
onOk={() => {
// 这里就可以使用 formRef.current?.form 操作了
// 但是这里还有一个问题, 就是data的类型并没有给我们提示
// 这是因为我们在定义 form时并没有指定表单字段的类型
// 接下来我们将解决这个问题
formRef.current?.form.validateFields().then(data => {
console.log(data);
})
}}
>
<UserForm/>
</Modal>
</div>
);
};
上面的代码就已经能够完成 使用useImperativeHandle
和forwardRef
来传递ref
了, 接下来我们来优化一下 ref
的类型
这次只需要修改UserForm
就行, 改造如下
import { Input, Form, FormInstance } from 'antd';
import { forwardRef, useImperativeHandle } from 'react';
// 首先我们加入一个表单的字段类型定义
type IFormState = {
username: string;
password: string;
// ....
}
// 接下来的改动是这里, FormInstance也支持传入一个泛型, 这里就把我们的 IFormState 传进去
export type IRefUserForm = { form: FormInstance<IFormState>};
// 组件 props, 先占位
interface IProps {
}
// 多余的注释我就删了
const UserForm = forwardRef<IRefUserForm, IProps>((props, ref) => {
// 这里也要改造, 注意!!!
// 这里也可以传一个泛型, 把我们的表单字段定义传进去
const [form] = Form.useForm<IFormState>();
// ..... 下面都一样, 只要加两个泛型即可
});
export default UserForm;
通过增加IFormState
, 即可增加类型提示
接下来我们在改造一下ref
, 现在只有一个form
, 针对antd
的<Input>
组件, 再加入一个inputRef
试试
首先改一下IRefUserForm
的定义, 增加 input
属性, 值就是username
这个输入框的ref
然后再useRef
来创建一个 input的ref
, 最后把这个ref也暴露出去
让我们开始吧!
// 注意, 这里多引入了一个 `InputRef`, 这是一个类型定义, 用作useRef的类型, I是大写的
// 注意: 在antd 4.19 以下的版本中, 没有 InputRef这个类型, Input直接就是一个类型, 4.19可能并不准确, 这个是我记忆中的, 不一定准
// 但是现在都已经是V5版本了, 所以必须要使用 InputRef
import { Input, Form, FormInstance, InputRef } from 'antd';
import { forwardRef, useImperativeHandle } from 'react';
// ...
type IFormState = {
username: string;
password: string;
// ....
}
// 增加一个key input
// input 为什么会有一个null类型? 下面会讲
export type IRefUserForm = { form: FormInstance<IFormState>, input: InputRef | null};
// 组件 props, 先占位
interface IProps {
}
const UserForm = forwardRef<IRefUserForm, IProps>((props, ref) => {
// inputRef 是值, 小写的 i, InputRef是类型, 大写的 I
// 这里也要传一个null, 原因同上
const inputRef = useRef<InputRef>(null);
const [form] = Form.useForm<IFormState>();
useImperativeHandle(
ref,
() => ({
form: form,
// 注意这里, 我们使用的是 inputRef.current 而不是 input
// 是因为 input.current才是 InputRef的实例
// 而且还要注意 inputRef.current 的类型有两个及 InputRef|null
// 因为包含了一个null类型, 所以在IRefUserForm的类型定义时, input要额外增加一个 null的类型
input: inputRef.current
}),
[form, inputRef]
);
// .....
// 在这里把 inputRef 和 Input组件绑定
<Form.Item name="username" label="账号" >
<Input ref={inputRef}/>
</Form.Item>
// ......
});
export default UserForm;
在上面的改动中, 我们对 UserForm
提供的ref中增加了一个input
属性, 下面就在AddButton
中使用一下看看吧
就以打开弹窗是让<Input/>
自动聚焦为例
const AddButton: React.FC = () => {
import { Button, Modal } from 'antd';
import { useRef, useState } from 'react';
// 引用了 IRefUserForm 这个类型
import UserForm, { IRefUserForm } from './UserForm';
const [showAddModal, setShowAddModal] = useState(false);
const formRef = useRef<IRefUserForm>(null);
return (
<div>
<Button onClick={() => {
setShowAddModal(true);
// 改动在这里,通过在打开后操作 input上的 focus方法来实现
// 由于 antd的Modal没有提供类似 onOpen的方法,所以采用了setTimeout来实现
setTimeout(() => {
formRef.current?.input?.focus();
}, 200)
}}>
新增用户
</Button>
<Modal
open={showAddModal}
onCancel={() => setShowAddModal(false)}
>
<UserForm/>
</Modal>
</div>
);
};