你该知道的React Hooks

631

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

简介

本文主要针对React `16.8.x`提供的`hooks`使用加以介绍,更高版本的中的`hooks`暂无介绍

优势

  • 代码量少(最直观的体现)
  • 相较于 类组件 使用 HOC render props , hooks 更为简单方便的复用状态组件——状态处理逻辑复用(后面详细介绍
  • 100%向后兼容,与 类组件 可同时使用
  • 不需要考虑 this 相关的问题
  • 相较于 , 函数 更容易被机器理解
  • 相较于通过生命周期分割代码,不相干的逻辑放在同一个生命周期函数中,通过功能区分,放在不同的函数中,代码更容易理解和维护

劣势

  • 使用不当性能问题可能会比 类组件 要严重
  • 不能使用 decorator (装饰器)

官方API

useState

这个hooks应该是用的最多的了,

useState 可接受一个参数为,任意类型数值 或者 可以返回任意类型数值的函数

const [count, setCount] = useState(initialCount);
const [count, setCount] = useState(() => {
    //do something
    return resultCount
})

然后返回一个数组,数组第一个值为当前组件最新的 state ,只能通过数组的第二个值来更新,第二个值为更新 state 的工具函数(该工具函数可以接受一个 作为参数,为更新后 state 的结果;也可以接受一个 函数 作为参数,函数的参数为 state 的前一个值,如果返回值与当前 state 相同则不会重新渲染组件)

function Demo({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

tip

  • 类组件可以直接对this.state.something赋值(虽然不会更新视图),但在hooks 组件中不能直接对state赋值
  • hooks 组件state的更新函数同类组件setState一样是异步的,想要立即获取更新后的状态,可以使用useRef
  • useState不会同类组件setState一样是合并更新,而是直接覆盖更新
  • useState是基于useReducer的,建议:简单的数据结构,不同的状态放在不同的useState中,复杂的数据结构直接使用useReducer
  • useState参数为函数时,函数只会在初始化是执行一次
const formate = (data) => {
    // do some complicated things
    return result;
}

const [state, setState] = useState(formate(props.data)); // bad; formate每次渲染都要执行

const [state, setState] = useState(() => formate(props.data)); // good

useReducer

const [state, dispatch] = useReducer(reducer, initialState, initialAction);

useReducer 使用方式类似 redux ,适用于复杂状态的存储,同步更新状态,以及深层次更新组件状态。支持三个参数,第一个参数为 reducer 函数,第二个参数为初始状态initialState , 第三参数为一个函数(可选),用于对 initialState 初始化处理

const reducerFun = (state, action) => {
    switch (action.type) {
        case 'add':
            return {count: state.count + 1}
        case 'reduce':
            return {count: state.count + 1}
        default:
            return state
    }
}

const initialState = {count: 0};

const initialAction = (init) => {
    return {
        count: init.count + 1
    }
}

function Demo() {
   const [state, dispatch] = useReducer(reducerFun, initialState, initialAction)
    
    return (
        <div>
            {state.count}
            <button onClick={() => dispatch({type: 'add'})}>
                +
            </button>
            <button onClick={() => dispatch({type: 'reduce'})}>
                -
            </button>
        </div>
        
    )
}

tip

  • 深层次更新组件状态,可以将 dispatch 作为 props 传给子组件用于状态更新
  • 使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。

useEffect

useEffect(didUpdate, deps);

useEffect 有支持两个参数,第一个参数为 effect (副作用)函数,每次render之后执行,,这个函数可以有返回值,倘若有返回值,返回值也必须是一个函数,姑且称它为 清除函数 ,会在组件被销毁时执行(这句话是片面的)。其实不单是在组件销毁时执行,当组件更新时,清除函数 的函数的执行时机会被放置到,新一次组件 render 之后执行,然后再执行 effect 函数中非 清除函数部分

如以下demo:

function Demo() {
    const [count, setCount] = useState(0)
    const [random, setRandom] = useState(0)
   
    return (
        <div>
            {count % 2 == 0 && <Child count={count} random={random}/>}
            <button onClick={() => setCount(s => s+1)}> + </button>
            <button onClick={() => setRandom(Math.random())}>random</button>
        </div>
        
    )
}

function Child(props) {
    useEffect(() => {
        console.log('mounted')
        return () => {console.log('unmount')}
    })

    console.log('render')
    return (<h1>{props.count} {props.random}</h1>)
}

在这里插入图片描述

可以看出当 Child 组件销毁时,执行了 清除函数Child 组件创建时,先 render 然后执行了 effect 函数。但是更新 Child 组件时先 render 然后执行了 清除函数 ,然后才是 effect 函数

第二个参数可选,为一个数组,组件重新渲染之后,当数组中有值发生了改变时便会执行副作用函数,否则不执行。

function Demo() {
   const [count, setCount] = useState(0)
   const [count1, setCount1] = useState(0)

   useEffect(() => {
       console.log('count') // 只有 count发生改变时才会打印,count1改变不会
   }, [count])
    
    return (
        <div>
            {count}
            <button onClick={() => setCount((s) => s + 1)}>
                count+
            </button>
            <button onClick={() => setCount1((s) => s + 1)}>
                count1+
            </button>
        </div>
        
    )
}

如果想要只在组件初始挂载或者卸载时执行副作用只需将第二个参数置为 []

function Demo() {
    useEffect(() => {
        console.log('只有初始挂载或者卸载时执行')
    }, [])

    return (
        <div> </div>
    )
}

tip

  • effect 函数的执行时机是在组件创建或更新 render 之后,如果想要在 render 时同步触发副作用可以使用 useLayoutEffect
  • 不同于 class组件 在各生命周期中的各种副作用的处理,建议将不同的副作用放置不同的 useEffect 中通过功能进行区分,便于阅读理解

useLayoutEffect

useLayoutEffect 的使用形式和 useEffect 是相同的,它们之间的唯一区别是: useLayoutEffect 会在所有 DOM 变更之后同步调用。于是,可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新。与 componentDidMountcomponentDidUpdate 的调用时机是一样的。因为是同步调用,可能会产生视觉更新阻塞的问题,所以尽可能使用标准的 useEffect 以避免阻塞视觉更新

tip

  • 执行 DOM 更新操作时 useLayoutEffect 会比 useEffect 更适合使用
  • 涉及使用逐帧动画 requestAnimationFrame 时,注意执行时机:useLayoutEffect > requestAnimationFrame > useEffect

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(), deps);

useMemouseEffect 两者语法同样是一致的,两者区别是 useEffect 执行的是副作用,一定是在渲染后执行的,useMemo 是需要返回值的,返回值直接参与渲染,因此 useMemo 是在渲染时执行的

先举个反例:

function Demo(props) {
    const [count, setCount] = useState(1);
    const calculate = (count) => {
        let num = null;
        // num = 炒鸡复杂的处理逻辑
        return num;
    }
    return (
        <div count={calculate(count)}>
        </div>
    )
}

Demo组件每次渲染的时候都会执行calculate这个炒鸡复杂的计算过程,而参数count偏偏没有改变,每次重复的计算,是不是很浪费?这时候就可以使用useMemo来优化了。 如下代码,

function Demo(props) {
    const [count, setCount] = useState(1);
    const number = useMemo(() => {
        let num = null;
        // num = 超级复杂的处理逻辑
        return num;
    }, [count])
    return (
        <div count={number}>
        </div>
    )
}

只有在count发生了改变才会重新来执行这个炒鸡复杂的计算,没有改变时就直接拿过来之前的计算结果来用,是不是很方便?

tip

  • 注意 deps 参数的设置,避免更新错误
  • 传入 useMemo 的函数会在渲染期间执行。请不要在函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo
  • useMemo的另一常规用法就是和Rect.memo搭配使用减少子组件重复渲染,后文会有详细介绍

useCallback

const memoizedCallback = useCallback(() => {/*do something*/}, deps);

useCallback 用法和用途与 useMemo 类似,同样支持两个参数,第一参数为要缓存的函数,第二个为判断是否更新的依赖数组,其主要区别在于 useCallback 返回值为函数只能用于缓存函数,useMemo 可以用于缓存值和函数。可用于缓存事件回调函数。

同样先举个反例:

function Demo(props) {
    const clickHandle = () => {
        let num = null;
        // num = 超级复杂的处理逻辑
        return num;
    }
    return (
        <div onClick={clickHandle}>
        </div>
    )
}

每次组件更新的时候 clickHandle 都需要重新定义,显然是没有必要的,因此可以引入useCallback:

function Demo(props) {
    const clickHandle = useCallback(() => {
        let num = null;
        // num = 超级复杂的处理逻辑
        return num;
    }, [])
    return (
        <div onClick={clickHandle}>
        </div>
    )
}

tip

  • 我们可以认为:useCallback(fn, deps) 等同于 useMemo(() => fn, deps)
  • useCallback的另一常规用法就是和Rect.memo搭配使用减少子组件重复渲染,后文会有详细介绍

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。

function Demo(props) {
    const box = useRef(null);
    return (
        <div onClick={clickHandle} ref={box}>
        </div>
    )
}

不同于类组件中的ref,它不仅仅适用了DOM元素和类组件的引用,在hooks 组件中可以使用其保存任何值,且在组件的整个生命周期内保持不变。可以直接修改.current 属性的值且不触发组件更新。

function Demo(props) {
    const num = useRef(0);
    const [evenNum, setEvenNum] = useState(0)

    const add = () => {
        num.current += 1;
        if(!(num.current & 1)) {
            setEvenNum(num.current)
        }
    }

    return (
        <h1 onClick={add}>
        {evenNum}
        </h1>
    )
}

在这里插入图片描述 如图,当单次数点击时数字不会更新,偶次数点击时数字更新


useImperativeHandle

useImperativeHandle(ref, () => ({}))

类组件中想要在父组件中想要获取子组件实例,只需通过ref属性直接获取,然后就可以调用子组件属性方法,而useImperativeHandle就提供了在hooks 组件中实现该功能的方法;

function Demo() {
    const child = useRef(null);
   
    return (
        <div>
            <Child ref={child}/>
            <button onClick={() => {child.current.add()}}>add</button>
        </div>
        
    )
}

function Child(props, ref) {
    const [count, setCount] = useState(0);

    useImperativeHandle(ref, () => ({
        add: () => setCount((s) => s + 1)
    }))

    return (
        <h1>
            {count}
        </h1>
    )
}

Child = React.forwardRef(Child)

useContext

const value = useContext(MyContext);

useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值,能够在hooks 组件中读取 context 的值以及订阅 context 的变化。

const MyContext = React.createContext('test')

function Demo() {
    const [count, setCount] = useState(0)
    return (
        <div>
            <MyContext.Provider value={count}>
                <Child/>
            </MyContext.Provider>
            <button onClick={() => setCount(s => s + 1)}>+</button>
        </div>
    )
}

function Child(props) {
    const myContextValue = useContext(MyContext)
    return (
        <h1>
            {myContextValue}
        </h1>
    )
}

tip

  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook触发重渲染,并使用最新传递给MyContext providercontext value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
  • useContext(MyContext) 相当于 static contextType = MyContext 在类中,或者 <MyContext.Consumer>
  • 搭配 useReducer 可以实现简易版 redux

memo

memo 不属于 hooks, 却是为 hooks 组件 量身定做的一个 API ,是一个 HOC ,只能用于 函数组件 不能用于 类组件

    function MyComponent(props) {}
    function areEqual(prevProps, nextProps) {
        /*
        如果把 nextProps 传入 render 方法的返回结果与
        将 prevProps 传入 render 方法的返回结果一致则返回 true,
        否则返回 false
        */
    }
    export default React.memo(MyComponent, areEqual);

类似 类组件React.PureComponent shouldComponentUpdate(),对传入组件的 props 进行浅对比或者自定义对比,来决定组件是否需要重新渲染,已达到性能优化的目的。

首先看一个demo:

const MyContext = React.createContext('test')

function Demo() {
    const [count, setCount] = useState(0);
    const [contextNum, setContextNum] = useState(0);
    const [childNum, setChildNum] = useState(0);

    const addChildNum = () => {
        setChildNum(s => s + 1)
    }

    const formateChildNum = (n) => ({value: n + 's'});

    return (
        <div>
            <h1>
                fatherNum: {count}
            </h1>
            <MyContext.Provider value={contextNum}>
                <Child childNum={formateChildNum(childNum)} addChildNum={addChildNum}/>
            </MyContext.Provider>
            <button onClick={() => setCount(s => s + 1)}>add fatherNum</button>
            <button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
        </div>
    )
}

function Child({childNum, addChildNum}) {
    const myContextNum = useContext(MyContext)
    console.log('render Child')
    return (
        <div>
            <h1>
                myContextNum: {myContextNum}
            </h1>
            <h1>
                childNum: {childNum.value}
            </h1>
            <button onClick={addChildNum}>add childNum</button>
        </div>
        
    )
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bODXNkjM-1589288472736)(./source/optimize1.gif)]
可以看出当父组件中fatherNum改变组件更新时Child组件也随之重新渲染了,尽管渲染前后并无变化,这显然是一次无意义的渲染。这只是一个简单的demo,父组件只有一个state改变,子组件只是多了一次渲染,或许无关痛痒,但是如果父组件stateprops比较多改变比较频繁,而子组件又十分的‘复杂’,却额外多余渲染 N 次,就在用户体验上就很可能带来很差的影响了。

首先我们引入memo,再次只改变fatherNum:

+ Child = memo(Child) 

function Child({childNum, addChildNum}) {
    const myContextNum = useContext(MyContext)
    console.log('render Child')
    return (
        <div>
            <h1>
                myContextNum: {myContextNum}
            </h1>
            <h1>
                childNum: {childNum.value}
            </h1>
            <button onClick={addChildNum}>add childNum</button>
        </div>
        
    )
}

在这里插入图片描述 可以看到,引入 memo 后改变 fatherNum,父组件重新渲染以后,子组件仍然触发了无意义的重渲染。和预想的并不一样。原因在于:虽然传给 Child 组件 props 的值没有什么改变,但是,由于每次hooks 组件重新渲染以后,组件函数会重新执行一遍,因此 propschildNum 变成了一个新的object 对象,同样 addChildNum 函数也被重新定义了;(内存空间变了)。这时就需要用到 useMemouseCallback了。

优化后

function Demo() {
    const [fatherNum, setFatherNum] = useState(0);
    const [contextNum, setContextNum] = useState(0);
    const [childNum, setChildNum] = useState(0);
    
    // const addChildNum = () => {
    //    setChildNum(s => s + 1)
    // }
    const addChildNum = useCallback(() => {
        setChildNum(s => s + 1)
    },[]);

    // const formateChildNum = (n) => ({value: n + 's'});

    const resultChildNum = useMemo(() => ({value: childNum + 's'}), [childNum]);

    return (
        <div>
            <h1>
                fatherNum: {fatherNum}
            </h1>
            <MyContext.Provider value={contextNum}>
                <Child childNum={resultChildNum} addChildNum={addChildNum}/>
            </MyContext.Provider>
            <button onClick={() => setFatherNum(s => s + 1)}>add fatherNum</button>
            <button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
        </div>
    )
}

在这里插入图片描述

自定义hooks

自定义hooks本质也是函数,不同于一般函数的是,其内部可以使用useStateAPI做状态管理。正是因为有了它存在,才有了hooks 组件 相对于 类组件 最大的优势 ———— 状态处理逻辑复用自定义hooks的存在允许我们将 UI组件无UI组件 相分离。
栗子

github.com/streamich/r… 一个不错的自定义hooks库,除了可以拿来用以外还可以参考源码,总结自己的经验也是很不错的。

tip

  • 自定义hooks共享复用的是状态处理逻辑,是逻辑,而不是单纯的状态本身,每 hooks 都是独立的

注意事项

  • 不要从常规 JavaScript 函数调用 hooks;
  • 不要在循环,条件或嵌套函数中调用 hooks;
  • 必须在组件的顶层调用 hooks;
  • 可以从 React 功能组件调用 hooks;
  • 可以从自定义 hooks 中调用 hooks;
  • 自定义 hooks 必须使用 use 开头,这是一种约定;
  • 建议将不需要propsstate的函数提到组件外部。组件每次都会重新渲染,函数每次重新定义,因此一些不必要的函数可以提到函数外面;

最后

本文是对 作者根据自己的经验以及网上收集到的资料,对 hooks 使用以及注意事项的介绍,可以满足日常开发使用的需要。如果想要详细了解 其内部的实现机制,与其去看他人消化后的产物,不如直接阅读源码:github.com/facebook/re…

看完感觉有一点点收获的话,还望不吝点赞~