# 0.前言

创建项目

create-react-app hook-ts-demo --template typescript

# 一.函数式组件中使用

  1. 方式一
import React from "react";

type UserInfo = {
  name: string,
  age: number,
};

export const User = ({ name, age }: UserInfo) => {
  return (
    <div className="App">
      <p>{name}</p>
      <p>{age}</p>
    </div>
  );
};

const user = <User name="vortesnail" age={25} />;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 也可以通过以下方式使用有类型约束的函数式组件:
import React from "react";

type UserInfo = {
  name: string,
  age: number,
};

export const User: React.FC<UserInfo> = ({ name, age }) => {
  return (
    <div className="User">
      <p>{name}</p>
      <p>{age}</p>
    </div>
  );
};

const user = <User name="vortesnail" age={25} />;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上述代码中不同之处在于:

export const User = ({ name, age }: UserInfo) => {};
export const User: React.FC<UserInfo> = ({ name, age }) => {};
1
2
  1. 使用函数式组件时需要将组件申明为 React.FC 类型,也就是 Functional Component的意思,另外props 需要申明各个参数的类型,然后通过泛型传递给 React.FC

虽然两种方式都差不多,但我个人更喜欢使用 React.FC 的方式来创建我的有类型约束的函数式组件,它还支持children 的传入,即使在我们的类型中并没有定义它:

export const User: React.FC<UserInfo> = ({ name, age, children }) => {
  return (
    <div className="User">
      <p>{name}</p>
      <p>{age}</p>
      <div>{children}</div>
    </div>
  );
};

const user = (
  <User name="vortesnail" age={25}>
    I am children text!
  </User>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 我们也并不需要把所有参数都显示地解构:
export const User: React.FC<UserInfo> = (props) => {
  return (
    <div className="User">
      <p>{props.name}</p>
      <p>{props.age}</p>
      <div>
        {/*仍可以拿到 children*/}
        {props.children}
      </div>
    </div>
  );
};

const user = (
  <User name="vortesnail" age={25}>
    I am children text!
  </User>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 二.useState 中使用

怎么使用 useState?

const [count, setCount] = useState < number > 0;
1
  1. 参数为基本类型时的常规使用:
import React, { useState } from "react";

const Counter: React.FC<{ initial: number }> = ({ initial = 0 }) => {
  const [count, setCount] = useState < number > initial;

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}></button>
      <button onClick={() => setCount(count - 1)}></button>
    </div>
  );
};

export default Counter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 参数为对象类型时的使用:
import React, { useState } from "react";

type ArticleInfo = {
  title: string,
  content: string,
};

const Article: React.FC<ArticleInfo> = ({ title, content }) => {
  const [article, setArticle] = useState < ArticleInfo > { title, content };

  return (
    <div>
      <p>Title: {article.title}</p>
      <section>{article.content}</section>
      <button
        onClick={() =>
          setArticle({
            title: "下一篇",
            content: "下一篇的内容",
          })
        }
      >
        下一篇
      </button>
    </div>
  );
};

export default Article;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. 在我们的参数为对象类型时,需要特别注意的是, setXxx 并不会像this.setState 合并旧的状态,它是完全替代了旧的状态,所以我们要实现合并,可以这样写(虽然我们以上例子不需要):
setArticle({
  title: "下一篇",
  content: "下一篇的内容",
  ...article,
});
1
2
3
4
5

# 三.useEffect 中使用

为啥使用 useEffect? 你可以把 useEffect看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

怎么使用 useEffect?

useEffect(() => {
...
return () => {...}
},[...])
1
2
3
4
  1. 每当状态改变时,都要重新执行 useEffect的逻辑:
import React, { useState, useEffect } from "react";

let switchCount: number = 0;

const User = () => {
  const [name, setName] = useState < string > "";
  useEffect(() => {
    switchCount += 1;
  });

  return (
    <div>
      <p>Current Name: {name}</p>
      <p>switchCount: {switchCount}</p>
      <button onClick={() => setName("Jack")}>Jack</button>
      <button onClick={() => setName("Marry")}>Marry</button>
    </div>
  );
};

export default User;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 即使每次状态都改变,也只执行第一次 useEffect 的逻辑:
useEffect(() => {
  switchCount += 1;
}, []);
1
2
3
  1. 根据某个状态是否变化来决定要不要重新执行:
const [value, setValue] = useState < string > "I never change";
useEffect(() => {
  switchCount += 1;
}, [value]);
1
2
3
4

因为 value 我们不会去任何地方改变它的值,所以在末尾加了[value]后,useEffect内的逻辑也只会执行第一次,相当于在 class 组件中执行了componentDidMount ,后续的 shouldComponentUpdate 返回全部是false

  1. 组件卸载时处理一些内存问题,比如清除定时器、清除事件监听:
useEffect(() => {
  const handler = () => {
    document.title = Math.random().toString();
  };

  window.addEventListener("resize", handler);

  return () => {
    window.removeEventListener("resize", handler);
  };
}, []);
1
2
3
4
5
6
7
8
9
10
11

# 四.useRef 中使用

为啥使用 useRef? 它不仅仅是用来管理DOM ref的,它还相当于this, 可以存放任何变量,很好的解决闭包带来的不方便性。

怎么使用 useRef?

const [count, setCount] = useState < number > 0;
const countRef = useRef < number > count;
1
2
  1. 闭包问题:想想看,我们先点击 加 按钮 3 次,再点 弹框显示 1 次,再点 加 按钮 2 次,最终alert会是什么结果?
import React, { useState, useEffect, useRef } from "react";

const Counter = () => {
  const [count, setCount] = useState < number > 0;

  const handleCount = () => {
    setTimeout(() => {
      alert("current count: " + count);
    }, 3000);
  };

  return (
    <div>
      <p>current count: {count}</p>
      <button onClick={() => setCount(count + 1)}></button>
      <button onClick={() => handleCount()}>弹框显示</button>
    </div>
  );
};
export default Counter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

结果是弹框内容为 current count: 3,为什么?

当我们更新状态的时候, React会重新渲染组件, 每一次渲染都会拿到独立的count 状态, 并重新渲染一个 handleCount 函数. 每一个 handleCount 里面都有它自己的count

  1. 那如何显示最新的当前 count呢?
const Counter = () => {
  const [count, setCount] = useState < number > 0;
  const countRef = useRef < number > count;

  useEffect(() => {
    countRef.current = count;
  });

  const handleCount = () => {
    setTimeout(() => {
      alert("current count: " + countRef.current);
    }, 3000);
  };

  //...
};

export default Counter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 因为变更 .current 属性不会引发组件重新渲染,根据这个特性可以获取状态的前一个值:
const Counter = () => {
  const [count, setCount] = useState < number > 0;
  const preCountRef = useRef < number > count;

  useEffect(() => {
    preCountRef.current = count;
  });

  return (
    <div>
      <p>pre count: {preCountRef.current}</p>
      <p>current count: {count}</p>
      <button onClick={() => setCount(count + 1)}></button>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们可以看到,显示的总是状态的前一个值:

  1. 操作 Dom 节点,类似 createRef()
import React, { useRef } from "react";

const TextInput = () => {
  const inputEl = useRef < HTMLInputElement > null;

  const onFocusClick = () => {
    if (inputEl && inputEl.current) {
      inputEl.current.focus();
    }
  };

  return (
    <div>
      <input type="text" ref={inputEl} />
      <button onClick={onFocusClick}>Focus the input</button>
    </div>
  );
};

export default TextInput;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 五.useMemo 中使用

为啥使用 useMemo?

useEffect 可以知道,可以通过向其传递一些参数来影响某些函数的执行。 React 检查这些参数是否已更改,并且只有在存在差异的情况下才会执行此。

useMemo做类似的事情,假设有大量方法,并且只想在其参数更改时运行它们,而不是每次组件更新时都运行它们,那就可以使用 useMemo来进行性能优化。

记住,传入useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect 的适用范畴,而不是 useMemo

怎么使用 useMemo?

function changeName(name) {
  return name + "给name做点操作返回新name";
}

const newName = useMemo(() => {
  return changeName(name);
}, [name]);
1
2
3
4
5
6
7
  1. 常规使用,避免重复执行没必要的方法:我们先来看一个很简单的例子,以下是还未使用 useMemo 的代码:
import React, { useState, useMemo } from "react";

// 父组件
const Example = () => {
  const [time, setTime] = useState < number > 0;
  const [random, setRandom] = useState < number > 0;

  return (
    <div>
      <button onClick={() => setTime(new Date().getTime())}>
        获取当前时间
      </button>
      <button onClick={() => setRandom(Math.random())}>获取当前随机数</button>
      <Show time={time}>{random}</Show>
    </div>
  );
};

type Data = {
  time: number,
};

// 子组件
const Show: React.FC<Data> = ({ time, children }) => {
  function changeTime(time: number): string {
    console.log("changeTime excuted...");
    return new Date(time).toISOString();
  }

  return (
    <div>
      <p>Time is: {changeTime(time)}</p>
      <p>Random is: {children}</p>
    </div>
  );
};

export default Example;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

在这个例子中,无论你点击的是 获取当前时间 按钮还是 获取当前随机数 按钮,<Show />这个组件中的方法changeTime都会执行。

但事实上,点击 获取当前随机数 按钮改变的只会是 children 这个参数,但我们的 changeTime 也会因为子组件的重新渲染而重新执行,这个操作是很没必要的,消耗了无关的性能。

  1. 使用 useMemo 改造我们的 <Show /> 子组件:
const Show: React.FC<Data> = ({ time, children }) => {
  function changeTime(time: number): string {
    console.log("changeTime excuted...");
    return new Date(time).toISOString();
  }

  const newTime: string = useMemo(() => {
    return changeTime(time);
  }, [time]);

  return (
    <div>
      <p>Time is: {newTime}</p>
      <p>Random is: {children}</p>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个时候只有点击 获取当前时间 才会执行 changeTime 这个函数,而点击 获取当前随机数 已经不会触发该函数执行了。

  1. 你可能会好奇, useMemo能做的难道不能用 useEffect 来做吗? 答案是否定的!如果你在子组件中加入以下代码:
const Show: React.FC<Data> = ({ time, children }) => {
  //...

  useEffect(() => {
    console.log("effect function here...");
  }, [time]);

  const newTime: string = useMemo(() => {
    return changeTime(time);
  }, [time]);

  //...
};
1
2
3
4
5
6
7
8
9
10
11
12
13

你会发现,控制台会打印如下信息:

changeTime excuted...
effect function here...
1
2

正如我们一开始说的:传入 useMemo的函数会在渲染期间执行。 在此不得不提React.memo ,它的作用是实现整个组件的 Pure功能:

const Show:React.FC<Data> = React.memo(({ time, children }) => {...}
1

所以简单用一句话来概括 useMemoReact.memo的区别就是:前者在某些情况下不希望组件对所有 props 做浅比较,只想实现局部Pure功能,即只想对特定的props做比较,并决定是否局部更新。

# 六.useCallback 中使用

为啥使用 useCallback? useMemouseCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于useMemo 返回的是函数运行的结果, useCallback返回的是函数。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

怎么使用 useCallback?

function changeName(name) {
  return name + "给 name 做点操作返回新 name";
}

const getNewName = useMemo(() => {
  return changeName(name);
}, [name]);
1
2
3
4
5
6
7
  1. 将之前 useMemo的例子,改一下子组件以下地方就 OK 了:
const Show: React.FC<Data> = ({ time, children }) => {
  //...
  const getNewTime = useCallback(() => {
    return changeTime(time);
  }, [time]);

  return (
    <div>
      <p>Time is: {getNewTime()}</p>
      <p>Random is: {children}</p>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# 七.useReducer 中使用

为什么使用 useReducer? 有没有想过你在某个组件里写了很多很多的 useState 是什么观感?比如以下:

const [name, setName] = useState < string > "";
const [islogin, setIsLogin] = useState < boolean > false;
const [avatar, setAvatar] = useState < string > "";
const [age, setAge] = useState < number > 0;
//...
1
2
3
4
5

怎么使用 useReducer?

import React, { useState, useReducer } from "react";

type StateType = {
  count: number,
};

type ActionType = {
  type: "reset" | "decrement" | "increment",
};

const initialState = { count: 0 };

function reducer(state: StateType, action: ActionType) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter({ initialCount = 0 }) {
  const [state, dispatch] = useReducer(reducer, { count: initialCount });

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
}

export default Counter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 八.useContext 中使用

为啥使用 useContext? 简单来说 Context 的作用就是对它所包含的组件树提供全局共享数据的一种技术。

怎么使用 useContext?

export const ColorContext = React.createContext({ color: '#1890ff' })
const { color } = useContext(ColorContext)
// 或
export const ColorContext = React.createContext(null)
<ColorContext.Provider value='#1890ff'>
<App />
</ColorContext.Provider>
// App 或以下的所有子组件都可拿到 value
const color = useContext(ColorContext) // '#1890ff'
1
2
3
4
5
6
7
8
9
  1. 根组件注册,所有子组件都可拿到注册的值:
import React, { useContext } from "react";

const ColorContext = React.createContext < string > "";

const App = () => {
  return (
    <ColorContext.Provider value="#1890ff">
      <Father />
    </ColorContext.Provider>
  );
};

const Father = () => {
  return <Child />;
};

const Child = () => {
  const color = useContext(ColorContext);
  return (
    <div style={{ backgroundColor: color }}>Background color is: {color}</div>
  );
};

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 配合 useReducer 实现Redux的代替方案:
import React, { useReducer, useContext } from "react";

const UPDATE_COLOR = "UPDATE_COLOR";

type StateType = {
  color: string,
};

type ActionType = {
  type: string,
  color: string,
};

type MixStateAndDispatch = {
  state: StateType,
  dispatch?: React.Dispatch<ActionType>,
};

const reducer = (state: StateType, action: ActionType) => {
  switch (action.type) {
    case UPDATE_COLOR:
      return { color: action.color };
    default:
      return state;
  }
};

const ColorContext =
  React.createContext <
  MixStateAndDispatch >
  {
    state: { color: "black" },
  };

const Show = () => {
  const { state, dispatch } = useContext(ColorContext);
  return (
    <div style={{ color: state.color }}>
      当前字体颜色为: {state.color}
      <button
        onClick={() =>
          dispatch && dispatch({ type: UPDATE_COLOR, color: "red" })
        }
      >
        红色
      </button>
      <button
        onClick={() =>
          dispatch && dispatch({ type: UPDATE_COLOR, color: "green" })
        }
      >
        绿色
      </button>
    </div>
  );
};

const Example = ({ initialColor = "#000000" }) => {
  const [state, dispatch] = useReducer(reducer, { color: initialColor });
  return (
    <ColorContext.Provider value={{ state, dispatch }}>
      <div>
        <Show />
        <button
          onClick={() =>
            dispatch && dispatch({ type: UPDATE_COLOR, color: "blue" })
          }
        >
          蓝色
        </button>
        <button
          onClick={() =>
            dispatch && dispatch({ type: UPDATE_COLOR, color: "lightblue" })
          }
        >
          轻绿色
        </button>
      </div>
    </ColorContext.Provider>
  );
};

export default Example;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

# 参考

[1] https://zhuanlan.zhihu.com/p/36116211

[2] 案例中的代码详见:https://github.com/Carloin/hooks-ts-demo (opens new window)