1. 前言

基础知识 :https://www.yuque.com/qzhou/learning/wo7r7c
之前写一直是用到哪查到哪写到哪,现在结合React源码总结一下相关知识点以及对应的写法

1.1 你能学到

  1. 组件声明中泛型传入的分别都是啥
  2. 有点跑题的知识点:ReactElement、JSX.ELement、ReactNode 分别都是什么,三者什么关系
  3. 常用的七个Hooks的TS类型声明源码

React   TS 手册 - 图1

2. 组件声明

  • 泛型接口

    2.1 类组件

    我开始接触前端的时候,就基本是函数组件了,所以类组件相关的TS几乎没有自己写过,如有笔误,欢迎指出~

创造的类组件需要extend React.Component<P, S={}>,泛型接口接收两个参数:

  1. props类型的定义
  2. state类型的定义

大概就是这样:

  1. interface IProps {
  2. name: string;
  3. }
  4. interface IState {
  5. age: number;
  6. }
  7. class App extends React.Component<IProps, IState> {
  8. state = {
  9. age: 0,
  10. };
  11. render() {
  12. return (
  13. <div>
  14. {this.state.count}
  15. {this.props.name}
  16. </div>
  17. );
  18. }
  19. }

如果是PureComponent的话就是extend React.PureComponent<P, S={},ss={}>,第三个参数是getSnapshotBeforeUpdate的返回值
都不是必选的

两者的差别:它们的主要区别是PureComponent中的shouldComponentUpdate 会根据情况来判断是否需要更新组件,可能就拒绝了,减少刷新从而可以在一定程度上提升性能。

2.2 函数组件

函数类型可以使用React.FunctionComponent<P={}>来定义,我一般都是用其简写React.FC<P={}>,参数就是传入 props 类型的定义
举一个例子:自己写的Button组件——为了缩小篇幅,减少了代码

  1. import React, {FC} from 'react'
  2. interface ButtonProps {
  3. classes?: string
  4. disabled?: boolean
  5. children: React.ReactNode //这个后面会提到
  6. }
  7. export const Button: FC<ButtonProps> = (props) => {
  8. const {
  9. classes,
  10. disabled,
  11. children,
  12. ...restProps
  13. } = props
  14. return (
  15. <button className={classes} disabled={disabled} {...restProps}>
  16. {children}
  17. </button>
  18. )
  19. }

你也可以这样简单一点的写:

  1. export const Button = (props: ButtonProps) => {
  2. //...
  3. return (
  4. <button className={classes} disabled={disabled} {...restProps}>
  5. {children}
  6. </button>
  7. )
  8. }

但我肯定还是推荐用上上面的FC,毕竟TS就是让你显式地具有类型诊断功能的

  • FC 显式定义这就是函数组件,返回的就是组件该返回的东西,而不会是工具函数什么的
  • FC 提供对函数组件中可能存在的属性类型检查和补全
  • 一些类型推断

    3. 内置对象类型以及方法

    3.1 React.ReactElement

    这个也就是前面提到的函数组件所return的东西,也就是编译为JS后,React.createElement这个方法返回的类型。React.cloneElement()返回的也是

    源码

    Ctrl+鼠标点击,进入到类型声明文件中查看源码
    1. interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    2. type: T;
    3. props: P;
    4. key: Key | null;
    5. }
    该接口接收三个属性值:
  1. type:这个ReactElement的类型。
  2. props:接收的参数
  3. key:用于方便比较组件变化的key

    3.1.1 JSXElementConstructor 方法

    源码

    这里面提到的JSXElementConstructor是一个方法
    1. type JSXElementConstructor<P> =
    2. | ((props: P) => ReactElement<any, any> | null)
    3. | (new (props: P) => Component<P, any>);

    3.2 JSX.Element

    源码

    从源码中可以发现
    1. declare global {
    2. namespace JSX {
    3. interface Element extends React.ReactElement<any, any> { }
    4. }
    5. }
    JSX.ELement就是从 React.ReactElement那继承过来的,但是也没有添加其他新的属性 —— 那不就是完全一样的嘛。也就是说两者完全可以互相赋值~

    3.3 React.ReactNode

    React.ReactNode是组件的render函数的返回值,前面在定义props中的children类型时用的也是这个

    源码

    ```typescript type ReactText = string | number; type ReactChild = ReactElement | ReactText; interface ReactNodeArray extends Array {} type ReactFragment = {} | ReactNodeArray; interface ReactPortal extends ReactElement { //源码中位置不在这,我方便大家看,搬到一起了 key: Key | null; children: ReactNode; } type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
  1. 这里联合了多个类型,其中就有`ReactChild`,里面又有`ReactElement`
  2. <a name="QVlx7"></a>
  3. ## ⭐上述三者的关系
  4. 至此我们可以捋清楚这下面三者的关系<br />`JSX.Element` = `ReactElement ` `ReactNode`<br />在赋值的时候可要小心~
  5. <a name="SumMv"></a>
  6. ## 3.4 CSSProperties
  7. `React.CSSProperties``React`基于`TypeScript`定义的CSS属性类型,可以将一个方法的返回值设置为该类型,也就是告诉你,这里就是接收CSS样式
  8. <a name="dRUD6"></a>
  9. ### 源码
  10. ```typescript
  11. export interface CSSProperties extends CSS.Properties<string | number> {
  12. /**
  13. * The index signature was removed to enable closed typing for style
  14. * using CSSType. You're able to use type assertion or module augmentation
  15. * to add properties or an index signature of your own.
  16. *
  17. * For examples and more information, visit:
  18. * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
  19. */
  20. }

还是我仓库中的例子

  1. import React, { FC, CSSProperties } from 'react'
  2. export interface ProgressProps {
  3. //...
  4. styles?: CSSProperties
  5. }
  6. const Progress: FC<ProgressProps> = (props) => {
  7. const {styles} = props
  8. return (
  9. <div className='zhou-progress-bar' style={styles}>
  10. //...
  11. </div>
  12. </div>
  13. )
  14. }
  15. export default Progress

4. 结合Hook

我现在基本都是写函数组件,所以都是Hook+TS,你们呢

4.1 useState

源码

  1. function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
  2. function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

可以看出来,分为两种情况:

  1. 有初始值
  2. 没初始值

如果有初始值,那么 React 会根据设置的 state 初始值来进行类型推断

  1. const [num] = useState(1)
  2. // 会将num自动推导为number类型

你也可也显式地定义类型

  1. const [num] = useState<numerb>(1)

特殊情况:初始值为null

  1. const [a, seta] = useState(null)
  2. seta({}) //类型“{}”提供的内容与签名“(prevState: null): null”不匹配 ts

这样是无法再更新他的值的,所以如果一开始就能确认 a 的类型,也需要一同显试声明

  1. const [a, seta] = useState<null | object>(null)
  2. seta({})

4.2 useEffect

源码

  1. function useEffect(effect: EffectCallback, deps?: DependencyList): void;
  2. // NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref<T>
  3. /**
  4. * `useImperativeHandle` customizes the instance value that is exposed to parent components when using
  5. * `ref`. As always, imperative code using refs should be avoided in most cases.
  6. *
  7. * `useImperativeHandle` should be used with `React.forwardRef`.
  8. *
  9. * @version 16.8.0
  10. * @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle
  11. */
  12. type EffectCallback = () => (void | Destructor);
  13. type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };
  14. type DependencyList = ReadonlyArray<any>;

useEffect接收一个副作用回调函数,该函数只能返回空值或者一个Destructor,也就是一个返回空值或者是{ [UNDEFINED_VOID_ONLY]: never }的方法;第二个可选参数是依赖数组
我好像还真没有在实际code中对useEffect有什么相关的类型声明…

4.3 useContext

接收一个 context对象(React.createContext 的返回值)并返回该 context的当前值。当前的 context值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop决定。

源码

  1. type Provider<T> = ProviderExoticComponent<ProviderProps<T>>;
  2. type Consumer<T> = ExoticComponent<ConsumerProps<T>>;
  3. interface Context<T> {
  4. Provider: Provider<T>;
  5. Consumer: Consumer<T>;
  6. displayName?: string | undefined;
  7. }
  8. function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T;

可以注意一下Context的接口定义~

例子

createContext的话可以手动设置一下类型,useContext大部分时候靠类型推断就好了

  1. interface Itheme {
  2. color: string;
  3. }
  4. const themeContext = React.createContext<Itheme>({ color: "blue" });
  5. const App = () => {
  6. const { color } = useContext(themeContext);
  7. return <div style={{ color }}>app</div>;
  8. };

4.4 useReducer

useState的替代方案。它接收一个形如 (state, action) => newStatereducer,并返回当前的 state以及与其配套的 dispatch 方法。

(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

源码

  1. function useReducer<R extends Reducer<any, any>, I>(
  2. reducer: R,
  3. initializerArg: I & ReducerState<R>,
  4. initializer: (arg: I & ReducerState<R>) => ReducerState<R>
  5. ): [ReducerState<R>, Dispatch<ReducerAction<R>>];

源码中有好几个形式的,但是大同小异,就不全部粘贴进来了

例子

还是从React 文档中找jsx例子,然后我再自己加上 TS

  1. type Tstate = { count: number };
  2. type Taction = {
  3. type: 'increment' | 'decrement';
  4. };
  5. const initialState = {count: 0};
  6. function reducer(state: Tstate, action: Taction ) {
  7. switch (action.type) {
  8. case 'increment':
  9. return {count: state.count + 1};
  10. case 'decrement':
  11. return {count: state.count - 1};
  12. default:
  13. throw new Error();
  14. }
  15. }
  16. function Counter() {
  17. const [state, dispatch] = useReducer<Tstate, Taction>(reducer, initialState);
  18. return (
  19. <>
  20. Count: {state.count}
  21. <button onClick={() => dispatch({type: 'decrement'})}>-</button>
  22. <button onClick={() => dispatch({type: 'increment'})}>+</button>
  23. </>
  24. );
  25. }

4.5 useRef

源码

  1. interface MutableRefObject<T> {
  2. current: T;
  3. }
  4. interface RefObject<T> {
  5. readonly current: T | null;
  6. }
  7. function useRef<T>(initialValue: T): MutableRefObject<T>;
  8. function useRef<T>(initialValue: T|null): RefObject<T>;
  9. function useRef<T = undefined>(): MutableRefObject<T | undefined>;

MutableRefObjectRefObject的区别中就可以看出声明类型时有无|null的区别:

  1. const r1 = React.useRef<HTMLDivElement>(null)
  2. const r2 = React.useRef<HTMLDivElement | null>(null)
  • r1.current是可变的
  • r2.current是只读的readonly

    (这里我看有些文章是写错了的,我看的有些疑惑,就去源码中看了一看,确实是他们写错了)

用法

官方文档里的 useRef 例子,我加上 TS

  1. function TextInputWithFocusButton() {
  2. const inputEl = useRef<HTMLInputElement>(null!);
  3. const onButtonClick = () => {
  4. // `current` 指向已挂载到 DOM 上的文本输入元素
  5. inputEl.current.focus(); //第五行
  6. };
  7. return (
  8. <>
  9. <input ref={inputEl} type="text" />
  10. <button onClick={onButtonClick}>Focus the input</button>
  11. </>
  12. );
  13. }

在第二行中用到了nulll!断言这里非空,不然第五行可能是会报错的

当然,是因为这里确实是保证了非空,如果是确实不能保证的情况可以使用ES6中的可选链~

4.6 useMemo

源码

  1. function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

把“创建”函数和依赖项数组作为参数传入 useMemo,返回一个 memoized 值。
从源码中也可以看出必须保证传入的函数返回值和最后返回的值的类型必须是一致的T

例子

  1. const memoizedString = useMemo<string>(() => name.toLowerCase(), [name]);

4.7 useCallback

useMemouseCallback效果非常相似,不过前者缓存的是值,后者缓存的是函数罢了。实际上你完全可以用useMemo实现useCallback的功能

源码

  1. function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生变化时才会重新执行回调函数,最终返回一个 memoized 回调函数。

例子

  1. let a:string = 'zhou'
  2. let b:number = 20
  3. type TdoSth = (a:string, b:number) => void
  4. const doSomething: TdoSth = (a, b) => console.log(`${a}:${b}`)
  5. const memoizedCallback = useCallback<TdoSth>(() => {
  6. doSomething(a, b)
  7. }, [a, b])

ps:虽说该方法理论上能提升性能,但是也不是能用就用的~以后再写一篇相关的文章


Hooks不止这些,但是掌握看类型源码的方法之后看其他的应该也没什么问题~

学习资源

  1. TypeScript-React-Starter
  2. React TypeScript 中文手册
  3. React Github
  4. React 文档

    总结

  • 有些时候 React 会帮我们自动推断出提供的参数,不需要我们手动设置类型,但是自己设置一下类型总是更为安全的~

    🌊如果有所帮助,欢迎点赞关注,一起进步⛵