• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

现在写 React 组件都是基于 TypeScript,所以如何给组件写类型也是很重要的。

这节我们就来学下 React 组件如何写 TypeScript 类型。

用 cra 创建个项目:

npx create-react-app --template typescript react-ts

我们平时用的类型在 @types/react 这个包里,cra 已经帮我们引入了。

JSX 的类型

在 App.tsx 里开始练习 TypeScript 类型:

interface AaaProps {
    name: string;
}

function Aaa(props: AaaProps) {
    return <div>aaa, {props.name}</div>;
}

function App() {
    return (
        <div>
            <Aaa name="guang"></Aaa>
        </div>
    );
}

export default App;

其实组件我们一般不写返回值类型,就用默认推导出来的。

React 函数组件默认返回值就是 JSX.Element。

我们看下 JSX.Element 的类型定义:

const content: JSX.Element = <div>aaa</div>;

可以看到它就是 React.ReactElement。

也就是说,如果你想描述一个 jsx 类型,就用 React.ReactElement 就好了。

比如 Aaa 组件有一个 content 的 props,类型为 ReactElement:

这样就只能传入 JSX。

跑一下:

npm run start

ReactElement 就是 jsx 类型,但如果你传入 null、number 等就报错了:

那如果有的时候就是 number、null 呢?

换成 ReactNode 就好了:

看下它的类型定义:

ReactNode 包含 ReactElement、或者 number、string、null、boolean 等可以写在 JSX 里的类型。

这三个类型的关系 ReactNode > ReactElement > JSX.Element。

所以,一般情况下,如果你想描述一个参数接收 JSX 类型,就用 ReactNode 就行。

函数组件的类型

前面的函数组件,我们都没明确定义类型:

其实它的类型是 FunctionComponent:

const Aaa: React.FunctionComponent<AaaProps> = (props) => {
    return (
        <div>
            aaa, {props.name}
            {props.content}
        </div>
    );
};

看下它的类型定义:

可以看到,FC 和 FunctionComponent 等价,参数是 Props,返回值是 ReactNode。

而且函数组件还可以写几个可选属性,这些用到了再说。

hook 的类型

接下来过一下 hook 的类型:

useState

先从 useState 开始:

一般用推导出的类型就行:

也可以手动声明类型:

useEffect 和 useLayoutEffect 这种没有类型参数的就不说了。

useRef

useRef 我们知道,可以保存 dom 引用或者其他内容。

所以它的类型也有两种。

保存 dom 引用的时候,参数需要传个 null:

不然会报错:

而保存别的内容的时候,不能传 null,不然也会报错,说是 current 只读:

为什么呢?

看下类型就知道了:

当你传入 null 的时候,返回的是 RefObject,它的 current 是只读的:

这很合理,因为保存的 dom 引用肯定不能改呀。

而不传 null 的时候,返回的 MutableRefObject,它的 current 就可以改了:

因为 ref 既可以保存 dom 引用,又可以保存其他数据,而保存 dom 引用又要加上 readonly,所以才用 null 做了个区分。

传 null 就是 dom 引用,返回 RefObject,不传就是其他数据,返回 MutableRefObject。

所以,这就是一种约定,知道传 null 和不传 null 的区别就行了。

useImperativeHandle

我们前面写过 forwardRef + useImperativeHandle 的例子,是这样的:

import { useRef } from "react";
import { useEffect } from "react";
import React from "react";
import { useImperativeHandle } from "react";

interface GuangProps {
    name: string;
}

interface GuangRef {
    aaa: () => void;
}

const Guang: React.ForwardRefRenderFunction<GuangRef, GuangProps> = (
    props,
    ref
) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => {
        return {
            aaa() {
                inputRef.current?.focus();
            },
        };
    }, [inputRef]);

    return (
        <div>
            <input ref={inputRef}></input>
            <div>{props.name}</div>
        </div>
    );
};

const WrapedGuang = React.forwardRef(Guang);

function App() {
    const ref = useRef<GuangRef>(null);

    useEffect(() => {
        console.log("ref", ref.current);
        ref.current?.aaa();
    }, []);

    return (
        <div className="App">
            <WrapedGuang name="guang" ref={ref} />
        </div>
    );
}

export default App;

forwardRef 包裹的组件会额外传入 ref 参数,所以它不是 FunctionComponent 类型,而是专门的 ForwardRefRenderFunction 类型。

它有两个类型参数,第一个是 ref 内容的类型,第二个是 props 的类型:

其实 forwardRef 也是这两个类型参数,所以写在 forwardRef 上也行:

import { useRef } from "react";
import { useEffect } from "react";
import React from "react";
import { useImperativeHandle } from "react";

interface GuangProps {
    name: string;
}

interface GuangRef {
    aaa: () => void;
}

const WrapedGuang = React.forwardRef<GuangRef, GuangProps>((props, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => {
        return {
            aaa() {
                inputRef.current?.focus();
            },
        };
    }, [inputRef]);

    return (
        <div>
            <input ref={inputRef}></input>
            <div>{props.name}</div>
        </div>
    );
});

useImperativeHanlde 可以有两个类型参数,一个是 ref 内容的类型,一个是 ref 内容扩展后的类型。

useImperativeHanlde 传入的函数的返回值就要求满足第二个类型参数的类型

不过一般没必要写,因为传进来的 ref 就已经是有类型的了,直接用默认推导的就行。

useReducer

useReducer 可以传一个类型参数也可以传两个:

当传一个的时候,是 Reducer<xx,yy> 类型,xx 是 state 的类型,yy 是 action 的类型。

当传了第二个的时候,就是传入的初始化函数参数的类型。

其余 hook

剩下的 hook 的类型比较简单,我们快速过一遍:

useCallback 的类型参数是传入的函数的类型:

useMemo 的类型参数是传入的函数的返回值类型:

useContext 的类型参数是 Context 内容的类型:

当然,这些都没必要手动声明,用默认推导的就行。

再就是 memo:

它可以直接用包裹的函数组件的参数类型:

也可以在类型参数里声明:

参数类型

回过头来,我们再来看传入组件的 props 的类型。

PropsWithChildren

前面讲过,jsx 类型用 ReactNode,比如这里的 content 参数:

如果你不想通过参数传入内容,可以在 children 里:

这时候就要声明 children 的类型为 ReactNode:

import React, { ReactNode } from "react";

interface CccProps {
    content: ReactNode;
    children: ReactNode;
}

function Ccc(props: CccProps) {
    return (
        <div>
            ccc,{props.content}
            {props.children}
        </div>
    );
}

function App() {
    return (
        <div>
            <Ccc content={<div>666</div>}>
                <button>7777</button>
            </Ccc>
        </div>
    );
}

export default App;

但其实没有必要自己写,传 children 这种情况太常见了,React 提供了相关类型:

type CccProps = PropsWithChildren<{
    content: ReactNode;
}>;

看下它的类型定义:

就是给 Props 加了一个 children 属性。

CSSProperties

有时候组件可以通过 props 传入一些 css 的值,这时候怎么写类型呢?

用 CSSProperties。

比如加一个 color 参数:

或者加一个 styles 参数:

可以看到,提示出了 css 的样式名,以及可用的值:

import React, { CSSProperties, PropsWithChildren, ReactNode } from "react";

type CccProps = PropsWithChildren<{
    content: ReactNode;
    color: CSSProperties["color"];
    styles: CSSProperties;
}>;

function Ccc(props: CccProps) {
    return (
        <div>
            ccc,{props.content}
            {props.children}
        </div>
    );
}

function App() {
    return (
        <div>
            <Ccc
                content={<div>666</div>}
                color="yellow"
                styles={{
                    backgroundColor: "blue",
                }}>
                <button>7777</button>
            </Ccc>
        </div>
    );
}

export default App;

HTMLAttributes

如果你写的组件希望可以当成普通 html 标签一样用,也就是可以传很多 html 的属性作为参数呢?

那可以继承 HTMLAttributes:

上图中可以看到,提示了很多 html 的属性。

import React, { HTMLAttributes } from "react";

interface CccProps extends HTMLAttributes<HTMLDivElement> {}

function Ccc(props: CccProps) {
    return <div>ccc</div>;
}

function App() {
    return (
        <div>
            <Ccc p>
                <button>7777</button>
            </Ccc>
        </div>
    );
}

export default App;

那 HTMLAttributes 的类型参数是干嘛的呢?

是其中一些 onClick、onMouseMove 等事件处理函数的类型参数:

当然,继承 HTMLAttributes 只有 html 通用属性,有些属性是某个标签特有的,这时候可以指定 FormHTMLAttributes、AnchorHTMLAttributes 等:

比如 a 标签的属性,会有 href:

ComponentProps

继承 html 标签的属性,前面用的是 HTMLAttributes:

其实也可以用 ComponentProps:

效果一样。

ComponentProps 的类型参数是标签名,比如 a、div、form 这些。

EventHandler

很多时候,组件需要传入一些事件处理函数,比如 clickHandler:

import React, { HTMLAttributes, MouseEventHandler } from "react";

interface CccProps {
    clickHandler: MouseEventHandler;
}

function Ccc(props: CccProps) {
    return <div onClick={props.clickHandler}>ccc</div>;
}

function App() {
    return (
        <div>
            <Ccc
                clickHandler={(e) => {
                    console.log(e);
                }}></Ccc>
        </div>
    );
}

export default App;

这种参数就要用 xxxEventHandler 的类型,比如 MouseEventHandler、ChangeEventHandler 等,它的类型参数是元素的类型。

或者不用 XxxEventHandler,自己声明一个函数类型也可以:

interface CccProps {
    clickHandler: (e: MouseEvent<HTMLDivElement>) => void;
}

案例代码上传了小册仓库。

总结

我们过了一遍写 React 组件会用到的类型:

  • ReactNode:JSX 的类型,一般用 ReactNode,但要知道 ReactNode、ReactElement、JSX.Element 的关系

  • FunctionComonent:也可以写 FC,第一个类型参数是 props 的类型

  • useRef 的类型:传入 null 的时候返回的是 RefObj,current 属性只读,用来存 html 元素;不传 null 返回的是 MutableRefObj,current 属性可以修改,用来存普通对象。

  • ForwardRefRenderFunction:第一个类型参数是 ref 的类型,第二个类型参数是 props 的类型。forwardRef 和它类型参数一样,也可以写在 forwardRef 上

  • useReducer:第一个类型参数是 Reducer<data 类型, action 类型>,第二个类型参数是初始化函数的参数类型。

  • PropsWithChildren:可以用来写有 children 的 props

  • CSSProperties: css 样式对象的类型

  • HTMLAttributes:组件可以传入 html 标签的属性,也可以指定具体的 ButtonHTMLAttributes、AnchorHTMLAttributes。

  • ComponentProps:类型参数传入标签名,效果和 HTMLAttributes 一样

  • EventHandler:事件处理器的类型

后面写 React 组件的时候,会大量用到这些 typescript 的类型。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
3.Hook的闭包陷阱的成因和解决方案
Next
5.React组件如何调试