1. 前言
基础知识 :https://www.yuque.com/qzhou/learning/wo7r7c
之前写一直是用到哪查到哪写到哪,现在结合React源码总结一下相关知识点以及对应的写法
1.1 你能学到
- 组件声明中泛型传入的分别都是啥
- 有点跑题的知识点:ReactElement、JSX.ELement、ReactNode 分别都是什么,三者什么关系
- 常用的七个Hooks的TS类型声明源码
2. 组件声明
创造的类组件需要extend React.Component<P, S={}>
,泛型接口接收两个参数:
- props类型的定义
- state类型的定义
大概就是这样:
interface IProps {
name: string;
}
interface IState {
age: number;
}
class App extends React.Component<IProps, IState> {
state = {
age: 0,
};
render() {
return (
<div>
{this.state.count}
{this.props.name}
</div>
);
}
}
如果是PureComponent
的话就是extend React.PureComponent<P, S={},ss={}>
,第三个参数是getSnapshotBeforeUpdate
的返回值
都不是必选的
两者的差别:它们的主要区别是PureComponent中的shouldComponentUpdate 会根据情况来判断是否需要更新组件,可能就拒绝了,减少刷新从而可以在一定程度上提升性能。
2.2 函数组件
函数类型可以使用React.FunctionComponent<P={}>
来定义,我一般都是用其简写React.FC<P={}>
,参数就是传入 props 类型的定义
举一个例子:自己写的Button组件——为了缩小篇幅,减少了代码
import React, {FC} from 'react'
interface ButtonProps {
classes?: string
disabled?: boolean
children: React.ReactNode //这个后面会提到
}
export const Button: FC<ButtonProps> = (props) => {
const {
classes,
disabled,
children,
...restProps
} = props
return (
<button className={classes} disabled={disabled} {...restProps}>
{children}
</button>
)
}
你也可以这样简单一点的写:
export const Button = (props: ButtonProps) => {
//...
return (
<button className={classes} disabled={disabled} {...restProps}>
{children}
</button>
)
}
但我肯定还是推荐用上上面的FC
,毕竟TS就是让你显式地具有类型诊断功能的
- FC 显式定义这就是函数组件,返回的就是组件该返回的东西,而不会是工具函数什么的
- FC 提供对函数组件中可能存在的属性类型检查和补全
- 一些类型推断
3. 内置对象类型以及方法
3.1 React.ReactElement
这个也就是前面提到的函数组件所return
的东西,也就是编译为JS后,React.createElement
这个方法返回的类型。React.cloneElement()
返回的也是源码
Ctrl
+鼠标点击,进入到类型声明文件中查看源码
该接口接收三个属性值:interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
- type:这个ReactElement的类型。
- props:接收的参数
- key:用于方便比较组件变化的key
3.1.1 JSXElementConstructor 方法
源码
这里面提到的JSXElementConstructor
是一个方法type JSXElementConstructor<P> =
| ((props: P) => ReactElement<any, any> | null)
| (new (props: P) => Component<P, any>);
3.2 JSX.Element
源码
从源码中可以发现declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
}
}
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;
这里联合了多个类型,其中就有`ReactChild`,里面又有`ReactElement`
<a name="QVlx7"></a>
## ⭐上述三者的关系
至此我们可以捋清楚这下面三者的关系<br />`JSX.Element` = `ReactElement `⊂ `ReactNode`<br />在赋值的时候可要小心~
<a name="SumMv"></a>
## 3.4 CSSProperties
`React.CSSProperties`是`React`基于`TypeScript`定义的CSS属性类型,可以将一个方法的返回值设置为该类型,也就是告诉你,这里就是接收CSS样式
<a name="dRUD6"></a>
### 源码
```typescript
export interface CSSProperties extends CSS.Properties<string | number> {
/**
* The index signature was removed to enable closed typing for style
* using CSSType. You're able to use type assertion or module augmentation
* to add properties or an index signature of your own.
*
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
}
还是我仓库中的例子:
import React, { FC, CSSProperties } from 'react'
export interface ProgressProps {
//...
styles?: CSSProperties
}
const Progress: FC<ProgressProps> = (props) => {
const {styles} = props
return (
<div className='zhou-progress-bar' style={styles}>
//...
</div>
</div>
)
}
export default Progress
4. 结合Hook
我现在基本都是写函数组件,所以都是Hook+TS,你们呢
4.1 useState
源码
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
可以看出来,分为两种情况:
- 有初始值
- 没初始值
如果有初始值,那么 React 会根据设置的 state 初始值来进行类型推断
const [num] = useState(1)
// 会将num自动推导为number类型
你也可也显式地定义类型
const [num] = useState<numerb>(1)
特殊情况:初始值为null
const [a, seta] = useState(null)
seta({}) //类型“{}”提供的内容与签名“(prevState: null): null”不匹配 ts
这样是无法再更新他的值的,所以如果一开始就能确认 a 的类型,也需要一同显试声明
const [a, seta] = useState<null | object>(null)
seta({})
4.2 useEffect
源码
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
// NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref<T>
/**
* `useImperativeHandle` customizes the instance value that is exposed to parent components when using
* `ref`. As always, imperative code using refs should be avoided in most cases.
*
* `useImperativeHandle` should be used with `React.forwardRef`.
*
* @version 16.8.0
* @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle
*/
type EffectCallback = () => (void | Destructor);
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };
type DependencyList = ReadonlyArray<any>;
useEffect
接收一个副作用回调函数,该函数只能返回空值或者一个Destructor
,也就是一个返回空值或者是{ [UNDEFINED_VOID_ONLY]: never }
的方法;第二个可选参数是依赖数组
我好像还真没有在实际code中对useEffect有什么相关的类型声明…
4.3 useContext
接收一个 context
对象(React.createContext
的返回值)并返回该 context
的当前值。当前的 context
值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop
决定。
源码
type Provider<T> = ProviderExoticComponent<ProviderProps<T>>;
type Consumer<T> = ExoticComponent<ConsumerProps<T>>;
interface Context<T> {
Provider: Provider<T>;
Consumer: Consumer<T>;
displayName?: string | undefined;
}
function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T;
例子
createContext
的话可以手动设置一下类型,useContext
大部分时候靠类型推断就好了
interface Itheme {
color: string;
}
const themeContext = React.createContext<Itheme>({ color: "blue" });
const App = () => {
const { color } = useContext(themeContext);
return <div style={{ color }}>app</div>;
};
4.4 useReducer
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer
,并返回当前的 state
以及与其配套的 dispatch
方法。
(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
源码
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I & ReducerState<R>,
initializer: (arg: I & ReducerState<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
例子
还是从React 文档中找jsx例子,然后我再自己加上 TS
type Tstate = { count: number };
type Taction = {
type: 'increment' | 'decrement';
};
const initialState = {count: 0};
function reducer(state: Tstate, action: Taction ) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer<Tstate, Taction>(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
4.5 useRef
源码
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T | null;
}
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T|null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
从MutableRefObject
和RefObject
的区别中就可以看出声明类型时有无|null
的区别:
const r1 = React.useRef<HTMLDivElement>(null)
const r2 = React.useRef<HTMLDivElement | null>(null)
r1.current
是可变的r2.current
是只读的readonly
(这里我看有些文章是写错了的,我看的有些疑惑,就去源码中看了一看,确实是他们写错了)
用法
官方文档里的 useRef 例子,我加上 TS
function TextInputWithFocusButton() {
const inputEl = useRef<HTMLInputElement>(null!);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus(); //第五行
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
在第二行中用到了nulll!
断言这里非空,不然第五行可能是会报错的
当然,是因为这里确实是保证了非空,如果是确实不能保证的情况可以使用ES6中的可选链~
4.6 useMemo
源码
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
把“创建”函数和依赖项数组作为参数传入 useMemo,返回一个 memoized 值。
从源码中也可以看出必须保证传入的函数返回值和最后返回的值的类型必须是一致的T
例子
const memoizedString = useMemo<string>(() => name.toLowerCase(), [name]);
4.7 useCallback
useMemo
和 useCallback
效果非常相似,不过前者缓存的是值,后者缓存的是函数罢了。实际上你完全可以用useMemo
实现useCallback
的功能
源码
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生变化时才会重新执行回调函数,最终返回一个 memoized 回调函数。
例子
let a:string = 'zhou'
let b:number = 20
type TdoSth = (a:string, b:number) => void
const doSomething: TdoSth = (a, b) => console.log(`${a}:${b}`)
const memoizedCallback = useCallback<TdoSth>(() => {
doSomething(a, b)
}, [a, b])
ps:虽说该方法理论上能提升性能,但是也不是能用就用的~以后再写一篇相关的文章
Hooks不止这些,但是掌握看类型源码的方法之后看其他的应该也没什么问题~
学习资源
- 有些时候 React 会帮我们自动推断出提供的参数,不需要我们手动设置类型,但是自己设置一下类型总是更为安全的~
🌊如果有所帮助,欢迎点赞关注,一起进步⛵