醉丶春风的Blog

千里之行, 始于足下



在typescript中使用useImperativeHandle 传递 ref的方法


在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;

上面只是两个最基础的组件, 如果现在我想在ModalonOk事件中处理表单数据, 可以选择的方法也很多, 这里只说传递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>
  );
};

上面的代码就已经能够完成 使用useImperativeHandleforwardRef来传递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>
  );
};

作者: 徐善通
地址: https://xstnet.com/article-160.html
声明: 除非本文有注明出处,否则转载请注明本文地址


我有话说



最新回复


正在加载中....

Copyrights © 2016-2019 醉丶春风 , All rights reserved. 皖ICP备15015582号-1