你可能不知道的 React + TypeScript 常用技巧

类型安全是我们在使用 TypeScript 时最大的优势,但是在 React 项目中,我们如何正确使用 TypeScript 呢,也许本文能给你带来一些帮助。

标注 React 组件的类型

假设我们有这样一个 Card 组件,其包含了 title 属性用于显示标题 及 onClick 点击事件:

tsx 复制代码
interface CardProps {
  className?: string
  children: React.ReactNode
  title?: string
  onClick?: (e: React.MouseEvent) => void
}

function Card(props: CardProps) {
  return (
    <div
      className="rounder-xl"
      onClick={props.onClick}
    >
      <span>{props.title}</span>
      {props.children}
    </div>
  )
}

如何传递 className

通常我们会为组件添加 className,在 React 中我们需要借助一些第三方库如 classnamesclsx 来处理:

tsx 复制代码
function Card(props: CardProps) {
  const { className, children, ...rest } = props
  return (
    <div className={clsx('rounder-xl', className)}>
      <span>{rest.title}</span>
      {children}
    </div>
  )
}

如何继承 HTML 原生标签的属性

我们可以使用 React.HTMLAttributes 来继承 HTML 原生标签的属性:

tsx 复制代码
// Bad ❌
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}

更加推荐的方式是使用 React.ComponentProps 来继承 HTML 原生标签的属性:

tsx 复制代码
// Good ✅
interface CardProps extends React.ComponentProps<'div'> {}

再具体一点,我们可以同时指定是否暴露 ref 属性给外部:

tsx 复制代码
interface CardProps extends React.ComponentPropsWithRef<'div'> {}

// Perfect ✅
interface CardProps extends React.ComponentPropsWithoutRef<'div'> {}

这样我们就可以让 CardProps 具有所有 div 标签的属性,同时不暴露 ref 属性给外部。

tsx 复制代码
interface CardProps extends React.ComponentPropsWithoutRef<'div'> {
  title?: string
}

function Card(props: CardProps) {
  const { className, children, title, ...rest } = props
  return (
    <div
      className={clsx('rounder-xl', className)}
      {...rest}
    >
      <span>{title}</span>
      {children}
    </div>
  )
}

CardProps 继承了 div 的属性后,通过 {...rest} 传参,我们省去了手动绑定 onClick 事件。

注意:上面将 className 单独解构,是为了防止传入 className 覆盖了组件内部的 className

范型组件

动态指定标签类型

如果我们需要动态指定 Card 组件最外层的标签类型:

tsx 复制代码
type CardProps<T extends React.ElementType> =
  React.ComponentPropsWithoutRef<T> &
    React.PropsWithChildren<{
      tag?: T
      title?: string
    }>

function Card<T extends React.ElementType = 'div'>(props: CardProps<T>) {
  const { tag: Component = 'div', children, title, ...rest } = props
  return (
    <Component {...rest}>
      <span>{title}</span>
      {children}
    </Component>
  )
}

这样我们可以通过 tag 属性控制 Card 组件最外层的标签类型,且可以通过范型来得到对应标签的类型提示。

此时默认 Card 最外层的标签为 div,我们仅可以传递 div 的属性,如果将 tag 属性指定为 button,则可以传递 button 的属性,如 disabled,这个属性是不在 div 上的。

在 .tsx 文件中为箭头函数添加范型

.tsx 文件中为箭头函数添加范型时,如果仅有 T,需要在后面添加 , 防止范型被识别为 JSX 语法:

这样编译器无法识别:

tsx 复制代码
const Card = <T>() => {}

这样就不会报错了:

tsx 复制代码
const Card = <T,>() => {}

实现可以动态渲染的列表组件

tsx 复制代码
interface CardListProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
}

function CardList<T>(props: CardListProps<T>) {
  return (
    <div className="flex flex-col space-y-4">
      {props.items.map(props.renderItem)}
    </div>
  )
}

上述代码定义了一个 CardList 组件,其接受一个 items 数组和一个 renderItem 函数,用于渲染列表中的每一项。

根据我们的数据类型进行渲染:

tsx 复制代码
interface Framework {
  id: number
  name: string
}

const data = [
  { id: 1, name: 'React' },
  { id: 2, name: 'Vue' },
  { id: 3, name: 'Angular' }
]

const App = () => {
  return (
    <CardList<Framework>
      items={data}
      renderItem={(framework) => {
        return (
          <div>
            <span>{framework.id}</span>
            <span>{framework.name}</span>
          </div>
        )
      }}
    />
  )
}

上述代码我们可以通过传入 Framework 类型来指定 CardList 组件的数据类型,renderItem 函数也会根据 Framework 类型拿到对应的参数类型。

这是一个常见的 React 进阶技术,叫做 Render Props,这方便了我们在父组件中进行自定义渲染,同时也实现了组件的关注点分离。在 React Native 中,FlatList 组件就是采用了这样的方式进行了封装。

类型缩窄

如果需要根据属性条件渲染组件,我们可以基于 TypeScript 的类型缩窄来实现:

tsx 复制代码
interface ButtonProps {
  text?: string
}

interface LinkProps {
  href?: string
  text?: string
}

function isLinkProps(props: ButtonProps | LinkProps): props is LinkProps {
  return 'href' in props
}

function Clickable(props: ButtonProps | LinkProps) {
  if (isLinkProps(props)) {
    // 此处的 Props 类型为 LinkProps
    return <a href={props.href}>{props.text}</a>
  } else {
    // 此处的 Props 类型为 ButtonProps
    return <button>{props.text}</button>
  }
}

上述代码可以根据是否传入 href 属性来渲染 a 标签或 button 标签。

事件处理

我们可以通过 React.MouseEventHandlerReact.ChangeEventHandler 来定义事件处理的类型:

tsx 复制代码
interface DemoProps {
  onClick: React.MouseEventHandler<HTMLButtonElement>
  onChange: React.ChangeEventHandler<HTMLInputElement>
}

// Good ✅
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
  console.log(e.currentTarget)
}

// Good ✅
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
  console.log(e.currentTarget.value)
}

// Bad ❌
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget)
}

// Bad ❌
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.currentTarget.value)
}

function Demo(props: DemoProps) {
  return (
    <div>
      <button onClick={props.onClick}>Click</button>
      <input onChange={props.onChange} />
    </div>
  )
}

使用 React.MouseEventReact.ChangeEvent 并不推荐,它们仅仅只能限定事件的参数类型。

相关推荐
王哲晓1 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4114 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v6 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云16 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:1379712058718 分钟前
web端手机录音
前端
齐 飞23 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹40 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0012 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html