React Hooks

  • State Hook
  • Effect Hook
  • 其他 Hook
  • 自定义 Hook
  • 组件逻辑复用
  • 规范和注意事项

一、 React Hooks的背景

1. 函数组件的特点

  1. 没有组件实例
  2. 没有生命周期
  3. 没有 state 和 setState,只能接收 props

2. class组件的问题

  1. 大型组件很难拆分和重构,很难测试(即class不易拆分)
  2. 相同业务逻辑,分散到各个方法中,逻辑混乱
  3. 复用逻辑变得复杂,如 Mixins、HOC、Render Props

3. React组件更容易用函数表达

  1. React提倡函数式编程
  2. 函数更灵活,更容易拆分和测试
  3. 函数组件太简单,需要增强能力---Hooks

二、State Hooks

让函数组件实现 state 和 setState

  1. 默认函数组件没有state
  2. 函数组件是一个纯函数,执行完即销毁,无法存储 state
  3. 需要 State Hook,即把 state 功能“钩”到纯函数中
// 写函数组件时,要引入 React,否则就是一个普通函数
import React, { useState } from 'react'

function ClickCounter() {
    // useState是一个 Hook,需要一个初始值,返回一个数组 [ state, setState ]
    const [count, setCount] = useState(0)
    
    return <div>
        <p>你点击了 { count }</p>
        <button onClick={() => setCount(count + 1)} ></button>
    </div>
}

1
2
3
4
5
6
7
8
9
10
11
12
13

1. useState使用总结

  1. useState(initialVal) 传入初始值,初始值可以是基本类型,也可以是对象,返回 [ state, setState ]
  2. 通过state获取值
  3. 通过setState修改值,遵循不可变值规范

2. Hooks 命名规范

  1. 规定所有的 Hooks 都用 use 开头,如 useXxx
  2. 自定义 Hook 也要 以 use 开头
  3. 非 Hooks 的地方,尽量不要使用 useXxx写法

三、Effect Hooks

让函数组件模拟生命周期

  • 默认函数组件没有生命周期
  • 函数组件是一个纯函数,执行完即销毁,自己无法实现生命周期
  • 使用 Effect Hook 把生命周期“钩”到纯函数中
// 写函数组件时,要引入 React,否则就是一个普通函数
import React, { useState, useEffect } from 'react'

function ClickCounter() {
    // useState是一个 Hook,需要一个初始值,返回一个数组 [ state, setState ]
    const [count, setCount] = useState(0)
    
    // 1. 模拟 componentDitMount 和 componentDitUpdate
    useEffect(() => {
        // TODO
    })
    
    // 2. 模拟 componentDitMount
    useEffect(() => {
        // TODO
    }, [ ] )
    
    // 3. 模拟 componentDitUpdate
    useEffect(() => {
        // TODO
    }, [ count ] )
    
    // 4. 模拟 componentWillUnMount
    useEffect(() => {
        // TODO
        return () => {
            // TODO
        }
    }, [ count ] )
    
    return <div>
        <p>你点击了 { 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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

1. useEffect使用总结

  1. 模拟 componentDitMount - useEffect依赖[ ]
  2. 模拟 componentDitUpdate - useEffect无依赖,或依赖[ a, b ]
  3. 模拟 componentWillUnMount - useEffect中返回一个函数

2. useEffect让纯函数有了副作用

  1. 默认情况下,执行纯函数,输入参数,返回结果,无副作用
  2. 所谓副作用,就是对函数之外造成影响,如设置全局定时任务
  3. 而组件需要副作用,所以需要 useEffect 钩,如纯函数

3. useEffect中返回的函数 fn

  1. useEffect依赖[ ],组件销毁时执行 fn,等价于 componentWillUnMount
  2. useEffect无依赖或依赖[ a, b ],组件更新时执行 fn,即下一次执行 useEffect 之前,就会执行 fn,无论更新或卸载

四、其他 Hooks

  1. useRef
  2. useContext
  3. useReducer
  4. useMemo
  5. useCallback

1. useRef

import React, { useRef, useEffect } from 'react'

function UseRef() {

    const btnRef = useRef(null)
    
    useEffect(() => {
        console.log(btnRef.current)
    }, [ ] )
    
    return <div>
        <button ref={btnRef}></button>
    </div>
}
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2. useContext

import React, { useContext } from 'react'

function ThemeButton() {
    const theme = useContexts(ThemeContext)
    return <button style={background: them.background}></button>
}

1
2
3
4
5
6
7

3. useReducer

import React, { useReducer } from 'react'

function reducer(state = 0, action) {
    switch(action) {
        case 'increment':
            return {count: state.count + 1}
        case 'decrement':
            return {count: state.count - 1}
        default:
            return state
    }
}

function ReducerDemo() {
    const [state, dispatch] = useReducer(reducer, initialState)
    
    return <div>
        <p>{state.count}</p>
        <button onClick={() => dispatch('increment')}>1</button>
        <button onClick={() => dispatch('decrement')}>1</button>
    </div>
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

useReducer和redux的区别

  1. useReducer是useState的代替方案,用于state复杂变化
  2. useReducer是单个组件状态管理,组件通讯还需要props
  3. redux是全局状态管理,多组件共享数据

4. useMemo做性能优化

import React, {memo, useMemo} from 'react'
// 子组件

const Child = memo( ( { userInfo } ) => {
    return <div>
        姓名:{userInfo.name}-年龄:{userInfo.age}
    </div>
})

// 父组件

function Parent() {
    const [count, setCount] = useState(0)
    const [name, setName] = useState('张三')
    
    // const userInfo = {name, age: 20}
    // 用 useMemo 缓存数据,有依赖
    const userInfo = useMemo(() => {
        return {
            name,
            age: 21
        }
    }, [name])
    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        <Child userInfo={userInfo}></Child>
    </div>
}

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

使用总结

  1. React中,父组件更新时,默认情况下会更新所有子组件
  2. class组件使用 SCU 和 PureComponent 做优化
  3. Hooks 中使用 useMemo,但优化的原理是相同的

5. useCallback做性能优化

import React, {memo, useState, useMemo} from 'react'
// 子组件

const Child = memo( ( { userInfo, onChange } ) => {
    return <div>
        姓名:{userInfo.name}-年龄:{userInfo.age}
        <input onChange={onChange}></input>
    </div>
})

// 父组件

function Parent() {
    const [count, setCount] = useState(0)
    const [name, setName] = useState('张三')
    
    // const userInfo = {name, age: 20}
    // 用 useMemo 缓存数据,有依赖
    const userInfo = useMemo(() => {
        return {
            name,
            age: 21
        }
    }, [name])
    
    // function onChange(e) {
    //     console.log(e.target.value)
    // }
    const onChnage = useCallback(e => {
        console.log(e.target.value)
    })
    
    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        <Child userInfo={userInfo} onChange={onChnage}></Child>
    </div>
}

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

使用总结

  1. useMemo缓存数据
  2. useCallback缓存函数
  3. 两者是 React Hooks 的常见优化策略

五、自定义 Hook

  • 封装通用的功能
  • 开发和使用第三方 Hooks
  • 自定义 Hook 带来了无限的扩展性,解耦代码
import React, { useState, useEffect } from 'react'
import axios from 'axios'

// 必须使用 useXxx 格式
function useAxios(url) {
    const [loading, setLoading] = useState(false)
    const [data, setData] = useState()
    const [error, setError] = useState()
    
    useEffect(() => {
        setLoading(true)
        axios.get(url)
              .then(res => setData(res))
              .catch(error => setError(error))
              .finally(() => setLoading(false))
    }, [])
    return [loading, data, error]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

七、Hooks 使用规范

  1. 只能用于React函数组件和自定义Hook中,其他地方不可以
  2. 只能用于顶层代码,不能再循环、判断中使用Hooks,且不能被打断,如没执行到hook就return
  3. eslint插件 eslint-plugin-react-hooks可以检测

八、Hooks 逻辑复用

1. Mixins、HOC和Render Props逻辑复用的弊端

(1)Mixins

  • 变量来源不清
  • 变量冲突
  • 调用顺序复杂混乱

(2)HOC

  • 组件层级嵌套过多,不易渲染,不易调试
  • HOC 会劫持 props,必须严格遵守规范,容易出现疏漏

(3)Render Props

  • 学习成本高,不易理解
  • 只能传递纯函数,而默认情况下纯函数功能有限

2. Hooks 做组件逻辑复用的好处

  • 完全符合 Hooks 原有规范,没有其他要求,易理解记忆
  • 变量来源和作用域明确
  • 不会产生组件嵌套

九、React Hooks 注意事项

  • useState初始化值,只有第一次有效(即re-render时,如果初始值变化,不通过setState修改,初始值不变。)
  • useEffect内部不能修改 state(依赖为 [ ]时,re-render时不会重新执行effect函数,此时修改state无效。如果有对应依赖时,re-render时会执行effect函数,修改state有效)
  • useEffect可能出现死循环(依赖为对象或数组时,原因是底层使用Object.is比较新旧数据时候有变化,比较数组或对象不相等,值类型和引用类型)