TypeScript在React组件中应用备忘录(一)

前言

在React中,forwardRefuseImperativeHandle钩子常被用于函数组件,以暴露组件内部的方法给父组件。通过明确地定义和使用类型,我们可以提高组件间的交互质量,确保类型安全,并利用自动补全等IDE特性。以下借用FancyButton组件暴露focus方法的实现过程来介绍是如何进行类型定义的。

定义组件Props和Ref接口

首先,定义组件的props和ref接口,这有助于在组件外部通过ref对象调用组件内部的方法时获得类型检查和自动补全的好处。

tsx 复制代码
import React, { useImperativeHandle, createRef } from 'react';

export interface FancyButtonRef {
  focus: () => void;
}

interface FancyButtonProps {
  label: string;
  onClick?: () => void;
}

实现FancyButton组件

FancyButton组件中,我们使用React.forwardRefuseImperativeHandle来暴露focus方法。

tsx 复制代码
import React, { useImperativeHandle, createRef } from 'react';

export interface FancyButtonRef {
  focus: () => void;
}

interface FancyButtonProps {
  label: string;
  onClick?: () => void;
}

const FancyButton = React.forwardRef<FancyButtonRef, FancyButtonProps>(
  (props, ref) => {
    const { label, onClick } = props;
    const buttonRef = createRef<HTMLButtonElement>();
    
    useImperativeHandle(ref, () => ({
      focus: () => {
        buttonRef.current?.focus();
      },
    }));

    return (
      <button ref={buttonRef} onClick={onClick}>
        {label}
      </button>
    );
  }
);

export default FancyButton;

在父组件中使用FancyButton

父组件通过ref访问FancyButtonfocus方法,在以下代码中展示了如何在实际场景中使用这种方法。

tsx 复制代码
import { useRef } from 'react';
import FancyButton from './FancyButton';
import type { FancyButtonRef } from './FancyButton';

const ParentComponent: FC = () => {
  const fancyButtonRef = useRef<FancyButtonRef>(null);

  const onButtonClick = () => {
    fancyButtonRef.current?.focus();
  };

  return (
    <div>
      <FancyButton ref={fancyButtonRef} label="点击我" />
      <button onClick={onButtonClick}>使旁边这个按钮聚焦</button>
    </div>
  );
};

export default ParentComponent;

FancyButton组件的实现代码中,将FancyButtonRefFancyButtonProps作为forwardRef的泛型参数引入,这种做法会导致在父组件中通过ref对象调用FancyButton内部方法时,提供了类型检查和自动补全功能,从而增强了代码的健壮性和开发效率。

forwardRef包裹组件的子组件使用useImperativeHandle

tsx 复制代码
import React, { useImperativeHandle, forwardRef, createRef } from 'react';

interface FancyButtonProps {
  label: string;
  onClick?: () => void;
}

export interface FancyButtonRef {
  focus: () => void;
}

const InnierFancyButton: React.FC<any> = ({ ref, label, onClick }) => {
  const buttonRef = createRef<HTMLButtonElement>();

  useImperativeHandle(ref, () => ({
    focus: () => {
      buttonRef.current?.focus();
    },
  }));

  return (
    <button ref={buttonRef} onClick={onClick}>
      {label}
    </button>
  );
};

const FancyButton = forwardRef<FancyButtonRef, FancyButtonProps>(
  (props, ref) => {
    return (
      <InnierFancyButton {...props} ref={ref} />
    );
  }
);

export default FancyButton;

在实际开发中,并不会在forwardRef包裹组件中直接使用useImperativeHandle,也许会在组件的子组件中使用useImperativeHandle

比如在 InnierFancyButton 组件中使用 useImperativeHandle ,那么InnierFancyButton子组件的props类型如何定义?

这个很好定义,使用 extends 继承 FancyButtonProps 类型,再使用React.ForwardedRef<FancyButtonRef> 定义 ref 的类型。

diff 复制代码
import React, { useImperativeHandle, forwardRef, createRef } from 'react';

interface FancyButtonProps {
  label: string;
  onClick?: () => void;
}

export interface FancyButtonRef {
  focus: () => void;
}

+ interface InnierFancyButtonProps extends FancyButtonProps {
+   ref: React.ForwardedRef<FancyButtonRef>
+ }

- const InnierFancyButton: React.FC<any> = ({ ref, label, onClick }) => {
+ const InnierFancyButton: React.FC<InnierFancyButtonProps> = ({ ref, label, onClick }) => {
  const buttonRef = createRef<HTMLButtonElement>();

  useImperativeHandle(ref, () => ({
    focus: () => {
      buttonRef.current?.focus();
    },
  }));

  return (
    <button ref={buttonRef} onClick={onClick}>
      {label}
    </button>
  );
};

const FancyButton = forwardRef<FancyButtonRef, FancyButtonProps>(
  (props, ref) => {
    return (
      <InnierFancyButton {...props} ref={ref} />
    );
  }
);

export default FancyButton;

使用 ComponentPropsWithoutRef 优化类型定义

InnierFancyButtonProps类型定义还可以优化一下,其实 FancyButtonProps 这个类型有点多余,可以使用 ComponentPropsWithoutRef 这个泛型类型优化掉。

diff 复制代码
import React, { useImperativeHandle, forwardRef, createRef } from 'react';

- interface FancyButtonProps {
-  label: string;
-  onClick?: () => void;
- }

export interface FancyButtonRef {
  focus: () => void;
}

- interface InnierFancyButtonProps extends FancyButtonProps {
+ interface InnierFancyButtonProps {
  ref: React.ForwardedRef<FancyButtonRef>
+ label: string;
+ onClick?: () => void;
}

const InnierFancyButton: React.FC<InnierFancyButtonProps> = ({ ref, label, onClick }) => {
  const buttonRef = createRef<HTMLButtonElement>();

  useImperativeHandle(ref, () => ({
    focus: () => {
      buttonRef.current?.focus();
    },
  }));

  return (
    <button ref={buttonRef} onClick={onClick}>
      {label}
    </button>
  );
};

- const FancyButton = forwardRef<FancyButtonRef, FancyButtonProps>(
+ const FancyButton = forwardRef<FancyButtonRef, ComponentPropsWithoutRef<typeof InnierFancyButton>>(
  (props, ref) => {
    return (
      <InnierFancyButton {...props} ref={ref} />
    );
  }
);

export default FancyButton;
相关推荐
Up九五小庞10 分钟前
开源埋点分析平台 ClkLog 本地部署 + Web JS 埋点测试实战--九五小庞
前端·javascript·开源
摘星编程33 分钟前
React Native + OpenHarmony:UniversalLink通用链接
javascript·react native·react.js
qq_177767371 小时前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos
烬头88211 小时前
React Native鸿蒙跨平台应用实现了onCategoryPress等核心函数,用于处理用户交互和状态更新,通过计算已支出和剩余预算
前端·javascript·react native·react.js·ecmascript·交互·harmonyos
2601_949593653 小时前
基础入门 React Native 鸿蒙跨平台开发:卡片组件
react native·react.js·harmonyos
天人合一peng3 小时前
Unity中button 和toggle监听事件函数有无参数
前端·unity·游戏引擎
方也_arkling4 小时前
别名路径联想提示。@/统一文件路径的配置
前端·javascript
毕设源码-朱学姐4 小时前
【开题答辩全过程】以 基于web教师继续教育系统的设计与实现为例,包含答辩的问题和答案
前端
qq_177767374 小时前
React Native鸿蒙跨平台剧集管理应用实现,包含主应用组件、剧集列表、分类筛选、搜索排序等功能模块
javascript·react native·react.js·交互·harmonyos
qq_177767374 小时前
React Native鸿蒙跨平台自定义复选框组件,通过样式数组实现选中/未选中状态的样式切换,使用链式调用替代样式数组,实现状态驱动的样式变化
javascript·react native·react.js·架构·ecmascript·harmonyos·媒体