what is?

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class。

Hooks是React16.8的新特性。能让你不必写class,也能用React的特性。

why we need?

简单说说为什么要搞出Hooks这个东西。
官方的解释是为了克服Class组件存在的三点弊端:

  • 有状态的逻辑难以复用(It’s hard to reuse stateful logic between components)

项目中公共的逻辑并没有一个很好的复用方式。复用逻辑在此之前的方案中可选高阶组件或者render props。但这些方案并不完美,一方面,引入了毫无意义的DOM节点,另一方面,组件可能在改写的时候重构量很大。
通过Hooks,就可以从组件中提取出有状态的逻辑,这样它还能独立被测试和重用。Hooks可以使你不用改变组件层级就能复用有状态逻辑。这点主要表现在自定义Hooks的使用中。

  • 复杂的组件不好理解(Complex components become hard to understand)

我们经常会维护那些起初简单,但是最终变成一堆无法管理的状态逻辑和副作用的组件。每一个生命周期方法通常包含了不相关的逻辑。、
比如说,在componentDidMount和componentDidUpdate中,组件发起数据请求。但是,这个相同的componentDidMount也可能包含了一些没有联系的逻辑,比如设置监听器,而这些又要在componentWillUnmount中被移除。相互联系的代码更改后被分开,但是毫无关联的代码却在同一个方法中。
useEffect的各种用法解决了这个问题。

  • Class足够迷惑(Classes confuse both people and machines)

这里Facebook官方认为,class是学习React的巨大障碍(eg:你得记得“bind”事件处理器。没有不稳定的语法建议,代码将十分冗长。人们很容易理解state,props,自顶向下数据流,却对class感到挣扎。函数组件和class组件的区别以及应什么时候来使用它们也让有经验的React开发者们争论不休
另外,概念上讲,Hooks使得React也更加拥抱函数。

how to be used?

基本的Hook

useState

这一定是学习接触的第一个Hook。
核心就是赋予函数组件以状态。Hook之所以叫钩子,可以理解成,把一些React能力钩进当前的函数组件(比如状态)。

  1. const [state, setState] = useState(initialState);

useState调用后的返回值是一个tuple元组。包含了当前的值以及设置此值的函数—setState(当然,这个函数名随意定义,函数变量引用而已)。
函数调用和class组件中this.setState类似有两种形式:1、setState(newValue); 2、 setState(prevValue=>(newValue))。同理,每次的当新的值依赖于旧值计算而得出时,推荐使用第二种形式;(原因参考

useEffect

这不一定,但很可能是很多人接触的最后一个Hook。(因为后面的旧不看了)
不过它确实很重要,还有点麻烦。
useEffect看名字即知道这个东西要处理副作用了。React官方讲的副作用指的是一些,修改DOM、网络请求、定时器等操作。

  1. useEffect(didUpdate, condition);

这里didUpdate是一个effect,确切的说,是一个函数。如果函数有返回值,返回值必须是一个没有返回值的函数。这个函数执行的是卸载任务。后文展开介绍。
这里condition是出发这个effect的执行条件,是一个数组。数组中的值变化,在那一轮渲染的结束就会触发一波effect的执行。
结合参考下在@types/react中index.d.ts中的定义更好理解参数的形式:

  1. // useEffect定义
  2. function useEffect(effect: EffectCallback, deps?: DependencyList): void;
  3. // 其参数1定义
  4. type EffectCallback = () => (void | (() => void | undefined));
  5. // 其参数2定义
  6. type DependencyList = ReadonlyArray<any>;

问题1 :执行时机

这里执行时机涉及两个:一个是effect的执行时机,一个是effect返回的函数,用来做清理的那个的执行时机。effect的执行时机很明确,就是在每次渲染结束后,所谓 ‘didUpdate’ 。
但是其返回的函数的执行时机我自己之前理解有偏差,我以为是组件卸载时,因为这个返回的函数的目的是用来做卸载的。当然,这也不完全是错的,假设这个组件只经历了一次渲染就被卸载了,那这个返回的函数确实是卸载前被执行。但是,如果函数组件需要多次被渲染时,这个返回的函数的执行时机是:(官方: previous effect is cleaned up before executing the next effect.在执行下一个 effect 之前,上一个 effect 就已被清除)就是说,这个返回的函数在effect执行之前被调用。
**

问题2: 关于执行时机,新的问题来了。

这段代码在第一次点击按钮后输出神马🐎?

  1. type IProps = {};
  2. const CaptureValue: React.FC<IProps> = (props) => {
  3. const [stateValue, setStateValue] = useState(0);
  4. useEffect(() => {
  5. console.log('did update', stateValue);
  6. return () => {
  7. console.log('should clean', stateValue);
  8. }
  9. })
  10. console.log('rendering', stateValue );
  11. return (
  12. <div>
  13. <button onClick={()=>{setStateValue(v=>v+1)}}> ABCDEFG</button>
  14. </div>
  15. );
  16. };

控制台输出的是?
rendering 1
should clean 0
did update 1

还是?
rendering 1
should clean 1
did update 1

后文Capture Value中回答….

问题3: 利用useEffect的第二个参数,模拟class生命周期

这个问题倒是相对简单了:
第二个参数传空数组,模拟componentDidMount;
第二个参数传非空数组,比如[X], 模拟componentDidUpdate(prevProps){ if(prev.X !== this.props.X){ effect }};
第一个参数返回函数,模拟componentWillUmount;
这里模拟这个词语并不是太好,使用Hooks本可以不和class组件做类比,所以,甭管模拟什么生命周期了,记住调用时机就够了。
**

useContext

  1. const value = useContext(MyContext);

useContext戏路较窄。
它接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 的 value 属性决定。
当组件上层最近的 更新时,该 Hook 会触发重新渲染,传给组件最新的value值。
需要注意的是,参数是Context对象本身而不是MyContext.Consumer。

内置的其他Hook

useReducer

官方认为这是useState的替代方案,但却是有点像Redux的替代方案!
看例子就全明白了:

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

以前有句话说:如果你不知道你的React项目项目需要不需要使用Redux,那么答案就是不需要!
没错,你还可以局部使用useReducer。

useCallback

官方有讲:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useMemo

这两个钩子类似的功能,同时也和useEffect有相似之处。先看下例子。

  1. const TestMemo: React.FC<Props> = (props) => {
  2. const [a, setA] = React.useState(0);
  3. const [nbr, setNbr] = React.useState(0);
  4. const res = React.useMemo(()=>{
  5. console.log('triggered ...');
  6. return 'changed' + new Date().getTime();
  7. }, [a]);
  8. return (
  9. <div>
  10. <p> result is {res} </p>
  11. <button onClick={()=>{setA(a=>a+1)}}> click add a</button>
  12. <button onClick={()=>{setNbr(nbr=>nbr+1)}}> click add nbr</button>
  13. </div>
  14. );
  15. };

和useEffect的第二个参数deps类似,当deps发生变化时,useMemo会在渲染阶段(不是渲染完成后)调用其函数,函数的返回值就在当前渲染过程中参与UI的计算。但是,和useEffect不同之处在于,函数里面的过程和返回不能涉及副作用。
这是一种性能优化的手段,目的是为了填补class组件中的shouldComponentUpdate方法能做的性能优化工作。正如上面例子中,不是所有触发更新的状态或者属性的变化,我都想更新组件或者子组件。

再看一个useCallback的例子:

  1. function Parent() {
  2. const [count, setCount] = useState(1);
  3. const [val, setVal] = useState('');
  4. const callback = useCallback(() => {
  5. return doSth(count);
  6. }, [count]);
  7. return <div>
  8. <h4>{count}</h4>
  9. <Child callback={callback}/>
  10. <div>
  11. <button onClick={() => setCount(count + 1)}>+</button>
  12. <input value={val} onChange={event => setVal(event.target.value)}/>
  13. </div>
  14. </div>;
  15. }
  16. function Child({ callback }) {
  17. const [count, setCount] = useState(() => callback());
  18. useEffect(() => {
  19. setCount(callback());
  20. }, [callback]);
  21. return <div>
  22. {count}
  23. </div>
  24. }

若这里没有使用useCallback,我们把doth(count)的计算结果用值传给Child组件,当然,功能是一样的。但,那么假如和count不相干的value变化了,父组件触发了更新,这个结果

useRef

和React.createRef()的用法几乎一致。

  1. const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象引用在组件的整个生命周期内保持不变。不变,很重要。

useImperativeHandle

这个Hooks通常和ForwardRef一起使用。用来对上层暴露组件内部的方法。(ForwardRef可以将父组件ref引用向下传递给子组件,处理高阶组件常用一些)。useImperativeHandle其实就是给第一个参数的ref对象,添加第二参数的是一个函数返回的对象的所有属性。

  1. function FancyInput(props, ref) {
  2. const inputRef = useRef();
  3. useImperativeHandle(ref, () => ({
  4. focus: () => {
  5. inputRef.current.focus();
  6. }
  7. }));
  8. return <input ref={inputRef} ... />;
  9. }
  10. FancyInput = forwardRef(FancyInput);

在本例中,渲染 的父组件可以调用 inputRef.current.focus()。
这个通常在使用antd.Form的时候可能会用到。

另外,官方的useLayoutEffectuseDebugValue没了解过。

useLayoutEffect

useLayoutEffect并不是没有用。
参考:https://reactjs.org/docs/hooks-reference.html#useimperativehandle
作用可以简单理解和useEffect像,但是核心区别是渲染时机不同:

useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。 useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致

自定义Hook

自定义Hook才是Hook的精髓。
上文中说过,class组件中存在一个问题是,我们很难把那些可复用的逻辑(纯逻辑),用一种完全UI不相关的方式独立出来。
比如我项目中有很多地方使用了同一功能,比如请求某接口。但是我期望在不同页面中用不同的UI展示,比如A页面,该接口返回的数据我用文本,B页面我想用表格。
当然,最为糟糕的解决方案是,将他们封装成很多组件。。。更好一点的思想是,我用高阶组件,把等着用不同UI渲染的组件用参数的形式传进去。确实好了很多,至少算是复用了逻辑。
但是问题还是存在,比如,增加了无意义的节点。另外,高阶组件通常比较复杂,很难阅读理解,后期扩展和维护的时候,可能要深入查看组件内部。
这时候自定义Hook的价值就体现出来了。就目前复用逻辑的三种方案里面(others are Hoc & render props),自定义Hook确实是最好的。

简单理解下自定义Hook,就是官方提供了一种机制,让你自己书写hook(use开头的函数就被认为是自定义hook)。利用这种机制、原生hook的各种特性来抽象出一些可复用的东西。这样就像可插拔,即用即拿的灵活性较之前提升了太多。当然,不强求一定要把UI和逻辑解耦。

官方文档Building Your Own Hooks足够详细,从Hoc到自定义Hook,看完会觉得很惊艳。有意思的是,这篇文档的最后一节叫useYourImagination,一方面遵循自定义hook的命名规范,有一方面含义深远—你的想象有多远,hook就能触及多远~

官方强调的两个注意点

段落就直接翻译了Rules of Hooks

只能在顶层调用Hooks

不要在循环,条件,或者嵌套的函数中调用Hooks。反之,必须在React函数的顶层使用Hooks。遵循这条规则,你就能保证在每次组件渲染中Hooks以相同的顺序调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在后文中会有更深入的解释。)

只能再React函数中调用Hooks

别在普通的JavaScript函数中调用Hooks。反之,你可以:

  • 在React函数组件中使用;
  • 在自定义Hooks中使用(下一讲我们就学习);

其他补充

Capture Value特性

在上文useEffect时,提到了Capture Value。
当你在使用hook发现自己拿到的状态不是最新的,You May Ask:Why am I seeing stale props or state inside my function?
这个特性可以简单的概括下:
每次render 都有自己的事件处理、自己的state和props、以及effects。
细品。。。

  • 每次渲染自己的state和props

    1. function Counter() {
    2. const [count, setCount] = useState(0);
    3. return (
    4. <div>
    5. <p>You clicked {count} times</p>
    6. <button onClick={() => setCount(count + 1)}>Click me</button>
    7. </div>
    8. );
    9. }

    每次点击导致了一次次渲染,但是渲染的是state的值,是将值传入进行渲染。

  • 都有自己的事件处理

  1. const App = () => {
  2. const [temp, setTemp] = React.useState(5);
  3. const log = () => {
  4. setTimeout(() => {
  5. console.log("3 秒前 temp = 5,现在 temp =", temp);
  6. }, 3000);
  7. };
  8. return (
  9. <div
  10. onClick={() => {
  11. log();
  12. setTemp(3);
  13. // 3 秒前 temp = 5,现在 temp = 5
  14. }}
  15. >
  16. xyz
  17. </div>
  18. );
  19. };

这里在触发log函数中定时器的那一次渲染,temp的值是5。所以,这个按钮的事件处理是属于“那一次的”。

  • 每次render都有自己的effect

所以上文中的例子

  1. type IProps = {};
  2. const CaptureValue: React.FC<IProps> = (props) => {
  3. const [stateValue, setStateValue] = useState(0);
  4. useEffect(() => {
  5. console.log('did update', stateValue);
  6. return () => {
  7. console.log('should clean', stateValue);
  8. }
  9. })
  10. console.log('rendering', stateValue );
  11. return (
  12. <div>
  13. <button onClick={()=>{setStateValue(v=>v+1)}}> ABCDEFG</button>
  14. </div>
  15. );
  16. };

点击后输出应该是:
rendering 1
should clean 0 // 这个‘卸载’卸载的是上一次的。
did update 1

Capture Value如何绕过?

利用 useRef 就可以绕过 Capture Value 的特性。
这是useRef比cerateRef强大的一点,它不只能持有DOM节点或者ReactNode的引用。
可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。

  1. function Example() {
  2. const [count, setCount] = useState(0);
  3. // 把值的引用用ref保存了
  4. const latestCount = useRef(count);
  5. useEffect(() => {
  6. // Set the mutable latest value
  7. latestCount.current = count;
  8. setTimeout(() => {
  9. // Read the mutable latest value
  10. console.log(`You clicked ${latestCount.current} times`);
  11. }, 3000);
  12. });
  13. // ...
  14. }