+关注继续查看

三、React Hooks


1. useState


默认情况下,React会为根据设置的state的初始值来自动推导state以及更新函数的类型:

image


如果已知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就会报错:

image


来看看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);
复制代码


这里我们没有给回调函数中的参数a定义类型,所以下面的调用方式都不会报错:

memoizedCallback("hello");
memoizedCallback(5)
复制代码


尽管add方法的两个参数都是number类型,但是上述调用都能够用执行。所以为了更加严谨,我们需要给回调函数定义具体的类型:

const memoizedCallback = useCallback(
  (a: number) => {
    add(a, b);
复制代码


这时候如果再给回调函数传入字符串就会报错了:

image


所有,需要注意,在使用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方法也是有类型的:

image


可以看到,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;


✨ RockUI 学习React Hooks和TypeScript实现仿Antd的react UI组件库,并通过此组件库在create-react-app的基础上实现了一套简单的应用,下方链接 样式解决方案——saas 测试——Jest框架 Icon库基于react-fontawesome github 文档 文档使用storybook进行展示 使用eslint和prettier对代码风格进行约束 使用见README.md,学习及开发中的问题与解决见STUDY_README.md 「使用 webpack 5 从0到1搭建React + TypeScript 项目环境」3. 资源模块
「使用 webpack 5 从0到1搭建React + TypeScript 项目环境」3. 资源模块
「使用 webpack 5 从0到1搭建React+TypeScript 项目环境」2. 集成 css、less 与 sass
「使用 webpack 5 从0到1搭建React+TypeScript 项目环境」2. 集成 css、less 与 sass
「使用 webpack5从0到1搭建React+TypeScript 项目环境」1. React 与 TypeScript 集成
「使用 webpack5从0到1搭建React+TypeScript 项目环境」1. React 与 TypeScript 集成
使用 React 和 TypeScript something 编写干净代码的10个必知模式
JavaScript 是一种松散的类型化语言,因此,它捕获了运行时。这样做的结果就是 JavaScript 错误被捕获得非常晚,这可能导致严重的 bug。
Vite2+React+TypeScript:搭建企业级轻量框架实践
前段时间写了个Vue3的工程项目用起来还不错,借此把它移植过来React这边,给大家介绍下Vite2+React+TypeScript如何合理搭建和使用周边插件,以及让他们组合到整个工程中去。