三、React Hooks
1. useState
默认情况下,React会为根据设置的state的初始值来自动推导state以及更新函数的类型:
如果已知state 的类型,可以通过以下形式来自定义state的类型:
const [count, setCount] = useState<number>(1) 复制代码
如果初始值为null,需要显式地声明 state 的类型:
const [count, setCount] = useState<number | null>(null); 复制代码
如果state是一个对象,想要初始化一个空对象,可以使用断言来处理:
const [user, setUser] = React.useState<IUser>({} as IUser); 复制代码
实际上,这里将空对象{}断言为IUser接口就是欺骗了TypeScript的编译器,由于后面的代码可能会依赖这个对象,所以应该在使用前及时初始化 user 的值,否则就会报错。
下面是声明文件中 useState 的定义:
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]; // convenience overload when first argument is omitted /** * Returns a stateful value, and a function to update it. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usestate */ function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>]; /** * An alternative to `useState`. * * `useReducer` is usually preferable to `useState` when you have complex state logic that involves * multiple sub-values. It also lets you optimize performance for components that trigger deep * updates because you can pass `dispatch` down instead of callbacks. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usereducer */ 复制代码
可以看到,这里定义两种形式,分别是有初始值和没有初始值的形式。
2. useEffect
useEffect的主要作用就是处理副作用,它的第一个参数是一个函数,表示要清除副作用的操作,第二个参数是一组值,当这组值改变时,第一个参数的函数才会执行,这让我们可以控制何时运行函数来处理副作用:
useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source] ); 复制代码
当函数的返回值不是函数或者effect函数中未定义的内容时,如下:
useEffect( () => { subscribe(); return null; } ); 复制代码
TypeScript就会报错:
来看看useEffect在类型声明文件中的定义:
// Destructors are only allowed to return void. type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never }; // NOTE: callbacks are _only_ allowed to return either void, or a destructor. type EffectCallback = () => (void | Destructor); // TODO (TypeScript 3.0): ReadonlyArray<unknown> type DependencyList = ReadonlyArray<any>; 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 */ 复制代码
可以看到,useEffect的第一个参数只允许返回一个函数。
3. useRef
当使用 useRef 时,我们可以访问一个可变的引用对象。可以将初始值传递给 useRef,它用于初始化可变 ref 对象公开的当前属性。当我们使用useRef时,需要给其指定类型:
const nameInput = React.useRef<HTMLInputElement>(null) 复制代码
这里给实例的类型指定为了input输入框类型。
当useRef的初始值为null时,有两种创建的形式,第一种:
const nameInput = React.useRef<HTMLInputElement>(null) nameInput.current.innerText = "hello world"; 复制代码
这种形式下,ref1.current是只读的(read-only),所以当我们将它的innerText属性重新赋值时会报以下错误:
Cannot assign to 'current' because it is a read-only property. 复制代码
那该怎么将current属性变为动态可变得的,先来看看类型声明文件中 useRef 是如何定义的:
function useRef<T>(initialValue: T): MutableRefObject<T>; // convenience overload for refs given as a ref prop as they typically start with a null value /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. * * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable * value around similar to how you’d use instance fields in classes. * * Usage note: if you need the result of useRef to be directly mutable, include `| null` in the type * of the generic argument. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useref */ 复制代码
这段代码的第十行的告诉我们,如果需要useRef的直接可变,就需要在泛型参数中包含'| null',所以这就是当初始值为null的第二种定义形式:
const nameInput = React.useRef<HTMLInputElement | null>(null); 复制代码
这种形式下,nameInput.current就是可写的。不过两种类型在使用时都需要做类型检查:
nameInput.current?.innerText = "hello world"; 复制代码
那么问题来了,为什么第一种写法在没有操作current时没有报错呢?因为useRef在类型定义式具有多个重载声明,第一种方式就是执行的以下函数重载:
function useRef<T>(initialValue: T|null): RefObject<T>; // convenience overload for potentially undefined initialValue / call with 0 arguments // has a default to stop it from defaulting to {} instead /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. * * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable * value around similar to how you’d use instance fields in classes. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useref */ 复制代码
从上useRef的声明中可以看到,function useRef的返回值类型化是MutableRefObject,这里面的T就是参数的类型T,所以最终nameInput 的类型就是React.MutableRefObject。
注意,上面用到了HTMLInputElement类型,这是一个标签类型,这个操作就是用来访问DOM元素的。
4. useCallback
先来看看类型声明文件中对useCallback的定义:
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T; /** * `useMemo` will only recompute the memoized value when one of the `deps` has changed. * * Usage note: if calling `useMemo` with a referentially stable function, also give it as the input in * the second argument. * * ```ts * function expensive () { ... } * * function Component () { * const expensiveResult = useMemo(expensive, [expensive]) * return ... * } * ``` * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usememo */ 复制代码
useCallback接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生变化时才会重新执行回调函数。来看一个例子:
const add = (a: number, b: number) => a + b; const memoizedCallback = useCallback( (a) => { add(a, b); }, [b] ); 复制代码
这里我们没有给回调函数中的参数a定义类型,所以下面的调用方式都不会报错:
memoizedCallback("hello"); memoizedCallback(5) 复制代码
尽管add方法的两个参数都是number类型,但是上述调用都能够用执行。所以为了更加严谨,我们需要给回调函数定义具体的类型:
const memoizedCallback = useCallback( (a: number) => { add(a, b); }, [b] ); 复制代码
这时候如果再给回调函数传入字符串就会报错了:
所有,需要注意,在使用useCallback时需要给回调函数的参数指定类型。
5. useMemo
先来看看类型声明文件中对useMemo的定义:
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; /** * `useDebugValue` can be used to display a label for custom hooks in React DevTools. * * NOTE: We don’t recommend adding debug values to every custom hook. * It’s most valuable for custom hooks that are part of shared libraries. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usedebugvalue */ 复制代码
useMemo和useCallback是非常类似的,但是它返回的是一个值,而不是函数。所以在定义useMemo时需要定义返回值的类型:
let a = 1; setTimeout(() => { a += 1; }, 1000); const calculatedValue = useMemo<number>(() => a ** 2, [a]); 复制代码
如果返回值不一致,就会报错:
const calculatedValue = useMemo<number>(() => a + "hello", [a]); // 类型“() => string”的参数不能赋给类型“() => number”的参数 复制代码
6. useContext
useContext需要提供一个上下文对象,并返回所提供的上下文的值,当提供者更新上下文对象时,引用这些上下文对象的组件就会重新渲染:
const ColorContext = React.createContext({ color: "green" }); const Welcome = () => { const { color } = useContext(ColorContext); return <div style={{ color }}>hello world</div>; }; 复制代码
在使用useContext时,会自动推断出提供的上下文对象的类型,所以并不需要我们手动设置context的类型。当前,我们也可以使用泛型来设置context的类型:
interface IColor { color: string; } const ColorContext = React.createContext<IColor>({ color: "green" }); 复制代码
下面是useContext在类型声明文件中的定义:
function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T; /** * Returns a stateful value, and a function to update it. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usestate */ 复制代码
7. useReducer
有时我们需要处理一些复杂的状态,并且可能取决于之前的状态。这时候就可以使用useReducer,它接收一个函数,这个函数会根据之前的状态来计算一个新的state。其语法如下:
const [state, dispatch] = useReducer(reducer, initialArg, init); 复制代码
来看下面的例子:
const reducer = (state, action) => { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } const Counter = () => { const initialState = {count: 0} const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 复制代码
当前的状态是无法推断出来的,可以给reducer函数添加类型,通过给reducer函数定义state和action来推断 useReducer 的类型,下面来修改上面的例子:
type ActionType = { type: 'increment' | 'decrement'; }; type State = { count: number }; const initialState: State = {count: 0} const reducer = (state: State, action: ActionType) => { // ... } 复制代码
这样,在Counter函数中就可以推断出类型。当我们视图使用一个不存在的类型时,就会报错:
dispatch({type: 'reset'}); // Error! type '"reset"' is not assignable to type '"increment" | "decrement"' 复制代码
除此之外,还可以使用泛型的形式来实现reducer函数的类型定义:
type ActionType = { type: 'increment' | 'decrement'; }; type State = { count: number }; const reducer: React.Reducer<State, ActionType> = (state, action) => { // ... } 复制代码
其实dispatch方法也是有类型的:
可以看到,dispatch的类型是:React.Dispatch,上面示例的完整代码如下:
import React, { useReducer } from "react"; type ActionType = { type: "increment" | "decrement"; }; type State = { count: number }; const Counter: React.FC = () => { const reducer: React.Reducer<State, ActionType> = (state, action) => { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: throw new Error(); } }; const initialState: State = {count: 0} const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> </> ); }; export default Counter;