你可能不知道的 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 并不推荐,它们仅仅只能限定事件的参数类型。

相关推荐
码客前端2 分钟前
理解 Flex 布局中的 flex:1 与 min-width: 0 问题
前端·css·css3
Komorebi゛3 分钟前
【CSS】圆锥渐变流光效果边框样式实现
前端·css
工藤学编程15 分钟前
零基础学AI大模型之CoT思维链和ReAct推理行动
前端·人工智能·react.js
徐同保16 分钟前
上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片
前端·javascript·pdf
怕浪猫17 分钟前
React从入门到出门第四章 组件通讯与全局状态管理
前端·javascript·react.js
博主花神17 分钟前
【React】扩展知识点
javascript·react.js·ecmascript
欧阳天风24 分钟前
用setTimeout代替setInterval
开发语言·前端·javascript
EndingCoder28 分钟前
箭头函数和 this 绑定
linux·前端·javascript·typescript
郑州光合科技余经理28 分钟前
架构解析:同城本地生活服务o2o平台海外版
大数据·开发语言·前端·人工智能·架构·php·生活
沐墨染30 分钟前
大型数据分析组件前端实践:多维度检索与实时交互设计
前端·elementui·数据挖掘·数据分析·vue·交互