hooks实践

# 简介

  • Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
  • 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook

# 解决的问题

  • 在组件之间复用状态逻辑很难,可能要用到render props和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 难以理解的 class,包括难以捉摸的this

# 注意事项

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
  • Hook 的使用范围:函数式的 React 组件中、自定义的 Hook 函数里;
  • Hook 必须写在函数的最外层,每一次 useState 都会改变其下标 (cursor),React 根据其顺序来更新状态;
  • 尽管每一次渲染都会执行 Hook API,但是产生的状态 (state) 始终是一个常量(作用域在函数内部);

# hooks比较

# React.mixin,高阶组件与hooks的比较

# React.mixin

React mixin 是通过React.createClass创建组件时使用的,现在主流是通过ES6方式创建react组件,官方因为mixin不好追踪变化以及影响性能,所以放弃了对其支持,同时也不推荐使用。mixin的原理其实就是将[mixin]里面的方法合并到组件的prototype上。

var logMixin = {
  alertLog:function(){
    alert('alert mixin...')
  },
  componentDidMount:function(){
    console.log('mixin did mount')
  }
}
var MixinComponentDemo = React.createClass({
  mixins:[logMixin],
  componentDidMount:function(){
    document.body.addEventListener('click',()=>{
      this.alertLog()
    })
    console.log('component did mount')
  }
})
// 打印如下
// component did mount
// mixin did mount
// 点击页面
// alert mixin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

可以看出来mixin就是将logMixn的方法合并到MixinComponentDemo组件中,如果有重名的生命周期函数都会执行(render除外,如果重名会报错)。但是由于mixin的问题比较多这里不展开讲。

# 高阶组件

组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。

例如:我们有个计时器和日志记录组件

class LogTimeComponent extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            index: 0
        }
    }
    componentDidMount(){
        this.timer = setInterval(()=>{
            this.setState({
                index: ++index
            })
        },1000)
        console.log('组件渲染完成----')
    }
    componentDidUpdate(){
       // console.log(`我背更新了${++Welcome to this.show}`)
    }
    componentWillUnmount(){
        clearInterval(this.timer)
        console.log('组件即将卸载----')
    }
    render(){
        return(
            <div>
                <span>{`我已经显示了:${this.state.index}s`}</span>
            </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

上面就实现了简单的日志和计时器组件。那么问题来了,假如有三个组件分别是LogComponent(需要记录日志)、SetTimeComponent(需要记录时间)、LogTimeShowComponent(日志和时间都需要记录),怎么处理呢?把上面逻辑 Ctrl+C 然后 Ctrl+V 吗?如果记录日志的文案改变需要每个组件都修改么?官方给我们提供了高阶组件(HOC)的解决方案:

function logTimeHOC(WrappedComponent,options={time:true,log:true}){
    return class extends React.Component{
        constructor(props){
            super(props);
            this.state = {
                index: 0
            }
        }
        componentDidMount(){
            options.time&&this.timer = setInterval(()=>{
                this.setState({
                    index: ++index
                })
            },1000)
            options.log&&console.log('组件渲染完成----')
        }
        componentDidUpdate(){
           //ptions.log&&console.log(`我背更新了${++Welcome to this.show}`)
        }
        componentWillUnmount(){
            this.timer&&clearInterval(this.timer)
            options.log&&console.log('组件即将卸载----')
        }
        render(){
            return(<WrappedComponent {...this.state} {...this.props}/>)
        }
    }
}
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

logTimeHOC就是一个函数,接受一个组件返回一个新的组件(其实高阶组件就是一个函数)。我们用这个高阶组件来构建我们上面的三个组件:

LogComponent:打印日志组件

class InnerLogComponent extends React.Component{
    render(){
        return(
            <div>我是打印日志组件</div>
        )
    }
}
// 使用高阶组件`logTimeHOC`包裹下 
export default logTimeHOC(InnerLogComponent,{log:true})
1
2
3
4
5
6
7
8
9

SetTimeComponent:计时组件

class InnerSetTimeComponent extends React.Component{
    render(){
        return(
            <div>
                <div>我是计时组件</div>
                <span>{`我显示了${this.props.index}s`}</span>
            </div>
        )
    }
}
// 使用高阶组件`logTimeHOC`包裹下 
export default logTimeHOC(InnerSetTimeComponent,{time:true})
1
2
3
4
5
6
7
8
9
10
11
12

LogTimeShowComponent:计时+打印日志组件

class InnerLogTimeShowComponent extends React.Component{
    render(){
        return(
            <div>
                <div>我是日志打印+计时组件</div>
            </div>
        )
    }
}
// 使用高阶组件`logTimeHOC`包裹下 
export default logTimeHOC(InnerLogTimeShowComponent)
1
2
3
4
5
6
7
8
9
10
11

这样不仅复用了业务逻辑提高了开发效率,同时还方便后期维护。当然上面的案例只是为了举例而写的案例,实际场景需要自己去合理抽取业务逻辑。高阶组件虽然很好用,但是也有一些自身的缺陷:

  • 高阶组件的props都是直接透传下来,无法确实子组件的props的来源。
  • 可能会出现props重复导致报错。
  • 组件的嵌套层级太深。
  • 会导致ref丢失。

# React Hook

上面说了很多,无非就是告诉我们已经有解决功能复用的方案了。为啥还要React Hook这个呢?上面例子可以看出来,虽然解决了功能复用但是也带来了其他问题。

由此官方带来React Hook,它不仅仅解决了功能复用的问题,还让我们以函数的方式创建组件,摆脱Class方式创建,从而不必在被this的工作方式困惑,不必在不同生命周期中处理业务。

import React,{ useState, useEffect } from 'react'
function useLogTime(data={log:true,time:true}){
    const [count,setCount] = useState(0);
    useEffect(()=>{
        data.log && console.log('组件渲染完成----')
        let timer = null;
        if(data.time){
            timer = setInterval(()=>{setCount(c=>c+1)},1000)
        } 
        return ()=>{
            data.log && console.log('组件即将卸载----')
            data.time && clearInterval(timer)
        }
    },[])
    return {count}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

通过React Hook的方式重新改写了上面日志时间记录高阶组件。那么我来重写下上面的三个组件:

LogComponent:打印日志组件

export default function LogComponent(){
    useLogTime({log:true})
    return(
        <div>我是打印日志组件</div>
    )
}
1
2
3
4
5
6

SetTimeComponent:计时组件

export default function SetTimeComponent (){
    const {count} = useLogTime({time:true})
    return(
        <div>
            <div>我是计时组件</div>
            <span>{`我显示了${count}s`}</span>
        </div>
    )
}
1
2
3
4
5
6
7
8
9

LogTimeShowComponent:计时+打印日志组件

javascript
export default function LogTimeShowComponent (){
    const {count} = useLogTime()
    return(
        <div>
            <div>我是日志打印+计时组件</div>
            <div>{`我显示了${count}s`}</div>
        </div>
    )
}
1
2
3
4
5
6
7
8
9
10

用React Hook实现的这三个组件和高阶组件一比较,是不是发现更加清爽,更加PF。将日志打印和记录时间功能抽象出一个useLogTime自定义Hook。如果其他组件需要打印日志或者记录时间,只要直接调用useLogTime这个自定义Hook就可以了

# 普通class与hooks的比较

假设现在要实现一个计数器的组件。如果使用组件化的方式,我们需要做的事情相对更多一些,比如说声明 state,编写计数器的方法等,而且需要理解的概念可能更多一些,比如 Javascript 的类的概念,this 上下文的指向等。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
  state = {
    count: 0
  }

countUp = () => {
  const { count } = this.state;
  this.setState({ count: count + 1 });
}

countDown = () => {
  const { count } = this.state;
  this.setState({ count: count - 1 });
}

render() {
  const { count } = this.state;
  return (
    <div>
      <button onClick={this.countUp}>+</button>
      <h1>{count}</h1>
      <button onClick={this.countDown}>-</button>
    </div>
  )
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
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
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
//const Counter = (props) => {
//const { title } = props;
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <h1>{count}</h1>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  )
}
ReactDOM.render(<Counter title="XXX"/>, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过上面的例子,显而易见的是 React Hooks 提供了一种简洁的、函数式(FP)的程序风格,通过纯函数组件和可控的数据流来实现状态到 UI 的交互(MVVM)

# 函数式与class的比较

# capture props

函数组件天生就是支持 props 的,基本用法上和 class 组件没有太大的差别。需要注意的两个区别是:

  • class 组件 props 挂载在 this 上下文中,而函数式组件通过形参传入;
  • 由于挂载位置的差异,class 组件中如果 this 发生了变化,那么 this.props 也会随之改变;而在函数组件里 props 始终是不可变的,因此遵守 capture value 原则(即获取的值始终是某一时刻的),Hooks 也遵循这个原则。

可以通过 useRef 来规避 capture value,因为 useRef 是可变的。

# state

class 组件 函数组件
创建状态 this.state = {} useState, useReducer
修改状态 this.setState() set function
更新机制 异步更新,多次修改合并到上一个状态,产生一个副本 同步更新,直接修改为目标状态
状态管理 一个 state 集中式管理多个状态 多个 state,可以通过 useReducer 进行状态合并(手动)
性能 如果 useState 初始化状态需要通过非常复杂的计算得到,请使用函数的声明方式,否则每次渲染都会重复执行

# 生命周期

  • componentDidMount / componentDidUpdate / componentWillUnMount

useEffect 在每一次渲染都会被调用,稍微包装一下就可以作为这些生命周期使用;

  • shouldComponentUpdate

通常我们优化组件性能时,会优先采用纯组件的方式来减少单个组件的渲染次数

class Button extends React.PureComponent {}

React Hooks 中可以采用 useMemo 代替,可以实现仅在某些数据变化时重新渲染组件,等同于自带了 shallowEqual 的 shouldComponentUpdate。

# 强制渲染 forceUpdate

由于默认情况下,每一次修改状态都会造成重新渲染,可以通过一个不使用的 set 函数来当成 forceUpdate。

const forceUpdate = () => useState(0)[1];

# Hooks API

10个API

# Basic Hooks

# useState

useState 是为了让 function component 具有 class component state 功能,

useState 接收一个初始化 state 值的参数,返回值为当前 state 以及更新 state 的函数。普通更新 / 函数式更新 state

使用方法如下:

const Index = () => {
  const [count, setCount] = useState(0);
  const [obj, setObj] = useState({ id: 1 });
  return (
    <>
      {/* 普通更新 */}
      <div>count:{count}</div>
      <button onClick={() => setCount(count + 1)}>add</button>

      {/* 函数式更新 */}
      <div>obj:{JSON.stringify(obj)}</div>
      <button
        onClick={() =>
          setObj((prevObj) => ({ ...prevObj, ...{ id: 2, name: "张三" } }))
        }
      >
        merge
      </button>
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

需要注意的是,通过 useState 得到的状态 count,在 Counter 组件中的表现为一个常量,每一次通过 setCount 进行修改后,又重新通过 useState 获取到一个新的常量

# useEffect

useEffect 的作用是执行一些副作用代码,比如 api 请求,DOM 元素修改等,它接收一个包含副作用代码的函数,该函数的返回值用于清除副作用。 useEffect 是 UI 已经渲染到屏幕上以后才会执行,因此副作用里面的代码是不会阻碍屏幕的渲染,与类组件相比,使用 useEffect 处理副作用后,屏幕会渲染地更快。

当传入一个 [] 时,表示 useEffect 只会执行一次,类似于 componentDidMount,但两者并不完全相等。

function useEffectDemo() {
  const [width, setWidth] = useState(window.innerWidth)
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  return <p>the innerWidth is {width}px</p>
}
1
2
3
4
5
6
7
8
9

useEffet 我们可以理解成它替换了componentDidMount, componentDidUpdate, componentWillUnmount 这三个生命周期,但是它的功能还更强大。

包含3个生命周期的代码结构

// => componentDidMount/componentDidUpdate
useEffect(() => {
    // 这里的代码块 等价于 componentDidMount
    // do something...
    
    // return的写法 等价于 componentWillUnmount 
    return () => {
       // do something...
    };
  },
  // 依赖列表,当依赖的值有变更时候,执行副作用函数,等价于 componentDidUpdate
  [ xxx,obj.xxx ]
);
1
2
3
4
5
6
7
8
9
10
11
12
13

注意:依赖列表是灵活的,有三种写法

  • 当数组为空 [ ],表示不会应为页面的状态改变而执行回调方法【即仅在初始化时执行,componentDidMount】,
  • 当这个参数不传递,表示页面的任何状态一旦变更都会执行回调方法
  • 当数组非空,数组里的值一旦有变化,就会执行回调方法

# 模拟React的生命周期

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。
  • componentDidMount:通过 useEffect 传入第二个参数为[]实现。
  • componentDidUpdate:通过 useEffect 传入第二个参数为空或者为值变动的数组。
  • componentWillUnmount:主要用来清除副作用。通过 useEffect 函数 return 一个函数来模拟。
  • shouldComponentUpdate:你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较。来模拟是否更新组件。
  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

其他使用场景

场景1:依赖了某些值,但是不要在初始化就执行回调方法,要让依赖改变再去执行回调方法

场景2:有一个getData的异步请求方法,要让其在初始化调用且点击某个按钮也可以调用;

//场景一
const firstLoad = useRef(true);
useEffect(() => {
  if (firstLoad.current) {
    firstLoad.current = false;
    return;
  }
  // do something...
}, [ xxx ]);
1
2
3
4
5
6
7
8
9
// 场景二
// 第一步:最开始 
const getData = async () => {
  const data = await xxx({ id: 1 });
  setDetail(data);
};
useEffect(() => {
  getData();
}, []);
const handleClick = () => {
  getData();
};
//React Hook useEffect has a missing dependency: 'getData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
//报错的意思就是:我需要 useEffect 需要添加getData依赖

// 第二步: useEffect 需要添加getData依赖
const getData = async () => {
  const data = await xxx({ id: 1 });
  setDetail(data);
};
useEffect(() => {
  getData();
}, [getData]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleClick = () => {
  getData();
};
//The 'getData' function makes the dependencies of useEffect Hook (at line 76) change on every render. 
//Move it inside the useEffect callback. 
//Alternatively, wrap the 'getData' definition into its own useCallback() Hook  react-hooks/exhaustive-deps
//报错的意思就是:这个组件只要一有更新触发了render, getData 的就会重新被定义,此时的引用不一样,会导致useEffect运行。

//第三步:用 useCallback 钩子来缓存它来提高性能
const getData = useCallback(async () => {
  const data = await xxx({ id: 1 });
  setDetail(data);
}, []);
useEffect(() => {
  getData();
}, [getData]);
const handleClick = () => {
  getData();
};
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

# useContext

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树 的逐层传递 props

useContext 是为了在 function 组件中使用类组件的 context (opens new window) API,使用方法很简单,首先创建一个 context:

const local = '🇨🇳'
const ThemeContext = React.createContext(local)
1
2

然后在 useContext hook 使用 context

function UseContextDemo() {
  const local = useContext(ThemeContext)
  return (
    <div>
      <p>local: {local}</p>
    </div>
  )
}
// render: 🇨🇳
1
2
3
4
5
6
7
8
9

在 class 组件中,如果想要修改 context 的值,我们会使用 Provider 提供 value 值,同样,在 function 组件中也可以:

const ThemeContext = React.createContext('🇨🇳')
function Context() {
  const local = useContext(ThemeContext)
  return <p>local: {local}</p>
}
function App() {
  return (
    <ThemeContext.Provider value={'🇺🇸'}>
      <Context />
    </ThemeContext.Provider>
  )
}
// render: 🇺🇸
1
2
3
4
5
6
7
8
9
10
11
12
13

多级传值:

const obj = {
  value: 1
};
const obj2 = {
  value: 2
};
const ObjContext = React.createContext(obj);
const Obj2Context = React.createContext(obj2);
const App = () => {
  return (
    <ObjContext.Provider value={obj}>
      <Obj2Context.Provider value={obj2}>
        <ChildComp />
      </Obj2Context.Provider>
    </ObjContext.Provider>
  );
};
// 子级
const ChildComp = () => {
  return <ChildChildComp />;
};
// 孙级或更多级
const ChildChildComp = () => {
  const obj = useContext(ObjContext);
  const obj2 = useContext(Obj2Context);
  return (
    <>
      <div>{obj.value}</div>
      <div>{obj2.value}</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

# 未使用 useContext

多了一层Consumer

import { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';

// 1. 使用 createContext 创建上下文
const UserContext = new createContext();

// 2. 创建 Provider
const UserProvider = props => {
  let [username, handleChangeUsername] = useState('');
  return (
    <UserContext.Provider value={{ username, handleChangeUsername }}>
      {props.children}
    </UserContext.Provider>
  );
};

// 3. 创建 Consumer
const UserConsumer = UserContext.Consumer;

// 4. 使用 Consumer 包裹组件
const Pannel = () => (
  <UserConsumer>
    {({ username, handleChangeUsername }) => (
      <div>
        <div>user: {username}</div>
        <input onChange={e => handleChangeUsername(e.target.value)} />
      </div>
    )}
  </UserConsumer>
);

const Form = () => <Pannel />;

const App = () => (
  <div>
    <UserProvider>
      <Form />
    </UserProvider>
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));

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

# 使用 useContext

import { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';

// 1. 使用 createContext 创建上下文
const UserContext = new createContext();

// 2. 创建 Provider
const UserProvider = props => {
  let [username, handleChangeUsername] = useState('');
  return (
    <UserContext.Provider value={{ username, handleChangeUsername }}>
      {props.children}
    </UserContext.Provider>
  );
};

const Pannel = () => {
  const { username, handleChangeUsername } = useContext(UserContext); // 3. 使用 Context
  return (
    <div>
      <div>user: {username}</div>
      <input onChange={e => handleChangeUsername(e.target.value)} />
    </div>
  );
};

const Form = () => <Pannel />;

const App = () => (
  <div>
    <UserProvider>
      <Form />
    </UserProvider>
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));

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

# 状态共享方案

说到状态共享,最简单和直接的方式就是通过 props 逐级进行状态的传递,这种方式耦合于组件的父子关系,一旦组件嵌套结构发生变化,就需要重新编写代码,维护成本非常昂贵。随着时间的推移,官方推出了各种方案来解决状态共享和代码复用的问题。

# Mixins

image-20201214195928438

React 中,只有通过 createClass 创建的组件才能使用 mixins。这种高耦合,依赖难以控制,复杂度高的方式随着 ES6 的浪潮逐渐淡出了历史舞台

# HOC

高阶组件源于函数式编程,由于 React 中的组件也可以视为函数(类),因此天生就可以通过 HOC 的方式来实现代码复用。可以通过属性代理和反向继承来实现,HOC 可以很方便的操控渲染的结果,也可以对组件的 props / state 进行操作,从而可以很方便的进行复杂的代码逻辑复用

import React from 'react';
import PropTypes from 'prop-types';

// 属性代理
class Show extends React.Component {
  static propTypes = {
    children: PropTypes.element,
    visible: PropTypes.bool,
  };

  render() {
    const { visible, children } = this.props;
    return visible ? children : null;
  }
}

// 反向继承
function Show2(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      if (this.props.visible === false) {
        return null;
      } else {
        return super.render();
      }
    }
  }
}

function App() {
	return (
  	<Show visible={Math.random() > 0.5}>hello</Show>
  );
}
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

Redux 中的状态复用是一种典型的 HOC 的实现,我们可以通过 compose 来将数据组装到目标组件中,当然你也可以通过装饰器的方式进行处理。

import React from 'react';
import { connect } from 'react-redux';

// use decorator
@connect(state => ({ name: state.user.name }))
class App extends React.Component{
  render() {
		return <div>hello, {this.props.name}</div>
  }
}

// use compose
connect((state) => ({ name: state.user.name }))(App);
1
2
3
4
5
6
7
8
9
10
11
12
13

# Render Props

显而易见,renderProps 就是一种将 render 方法作为 props 传递到子组件的方案,相比 HOC 的方案,renderProps 可以保护原有的组件层次结构。

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

// 与 HOC 不同,我们可以使用具有 render prop 的普通组件来共享代码
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }

  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

function App() {
  return (
    <div style={{ height: '100%' }}>
      <Mouse render={({ x, y }) => (
          // render prop 给了我们所需要的 state 来渲染我们想要的
          <h1>The mouse position is ({x}, {y})</h1>
        )}/>
    </div>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
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

# Hooks

通过组合 Hooks API 和 React 内置的 Context,从前面的示例可以看到通过 Hook 让组件之间的状态共享更清晰和简单。

# Additional Hooks

# useReducer

useReduceruseState 的一种代替方案,用于 state 之间有依赖关系或者比较复杂的场景。在某些场景下,useReducer 会比 useState 更适用,当state逻辑较复杂。我们就可以用这个钩子来代替useState,它的工作方式犹如 Redux;需要外置 reducer (全局),通过这种方式可以对多个状态同时进行控制;

useReducer 接收三个参数:

  • reducer:(state, action) => newState
  • initialArg: 初始化参数
  • Init: 惰性初始化,返回初始化数据

返回当前 state 以及配套的 dispatch 方法。首先看下 useReducer 处理简单的 state:

function UseReducerDemo() {
  const [count, dispatch] = useReducer(state => {
    return state + 1
  }, 0)
  return (
    <div>
      <p>count: {count}</p>
      <button
        onClick={() => {
          dispatch()
        }}
      >
        add
      </button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个例子和使用 useState 一样,都达到了计数的效果。 该例子中,useReducer 初始化了 count 值为 0,传入的 reducer 很简单,当接收到一个 dispatch 时,将 count 的值增加 1。

接下来我们看 useReducer 如何处理 state 有相互依赖的场景,还是从一个 demo 开始:

const CountApp = () => {
  const [count, setCount] = useState(0)
  const [frozen, setFrozen] = useState(false)
  const increase = () => {
    setCount(prevCount => {
      if (frozen) {
        return prevCount
      }
      return prevCount + 1
    })
  }
  useEffect(() => {
    increase()
    setFrozen(true)
    increase()
  }, [])
  return <p>count {count}</p>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在副作用中,我们执行 increase 先将 count 的值增加 1,然后执行 setFrozencount 的值 “冻住”,再执行 increasecount 的值增加 1,由于在 setCount 进行了判断,如果 frozentrue,则直接返回,否则增加 1,按照这样的思路,最后 count 的值应该为 1,但是事实上屏幕输出的是 2,为什么会出现这样的结果?

原因在于 function 组件的更新机制,当引入 hooks 以后,function 组件也拥有了 state 的功能,当我们 setState 时,UI 会重新渲染,但在这个过程中,有一点需要我们注意是: function 组件中,state 以及 props 都是静态值,不存在引用,或者也可以理解为 state 和 props 是一个 capture value,每次渲染的 state 和 props 都是独立的。

关于这点,可以查看 a-complete-guide-to-useeffect (opens new window) 了解更多。

在这个例子中,由于 useEffect 传入的依赖为 [],即该副作用只会在 UI 第一次渲染结束后执行一次。而在这次 render 中,count 的值为 0, frozen 值为 false,所以第二次执行 increase 时,frozen 值依然为 falsesetCount 返回的 prevCount 为 1 ,然后增加 1,这也就是为什么最后 render 的结果为 2,而不是 1。

对于 state 有相互依赖的情况,我们可以用 useReducer 来处理【要点】

const INCREASE = 'INCREASE'
const SET_FROZEN = 'SET_FROZEN'
const initialState = {
  count: 0,
  frozen: false
}
const CountApp = () => {
  const reducer = (state: any, action: any) => {
    switch (action.type) {
      case INCREASE:
        if (state.frozen) {
          return state
        }
        return {
          ...state,
          count: state.count + 1
        }

      case SET_FROZEN:
        return {
          ...state,
          frozen: action.frozen
        }
      default:
        return state
    }
  }
  const [state, dispath] = useReducer(reducer, initialState)
  useEffect(() => {
    dispath({ type: INCREASE })
    dispath({ type: SET_FROZEN, frozen: true })
    dispath({ type: INCREASE })
  }, [])
  return <p>current count: {state.count}</p>
}
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

当我们使用 useReducer 后,将 countfrozen 关联起来,执行 dispath({ type: SET_FROZEN, frozen: true }) 修改了 frozen 值 ,紧接着执行 dispath({ type: INCREASE }) 时,此时获取到的 frozen 值为 true,当然最后 render 结果就为 1 。

前面说了 useReduceruseState 的一种代替方案,那么如何使用 useState 实现呢,思路一样,只要将 count 与 frozen 放在一个 state 中即可解决

const CountApp = () => {
  const [state, setState] = useState({
    count: 0,
    frozen: false,
  });

  const increase = () => {
    setState(prevState => {
      if (prevState.frozen) {
        return prevState;
      }
      return {
        ...prevState,
        count: state.count + 1,
      };
    });
  };

  const setFrozen = () => {
    setState(prevState => {
      return {
        ...prevState,
        frozen: true,
      };
    });
  };

  useEffect(() => {
    increase();
    setFrozen();
    increase();
  }, []);

  return <p>current count: {state.count}</p>;
  // render:1
};
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

useReduceruseState 相比,优势在于可以将使用 reducer 将一些逻辑进行抽离,进行集中化管理

import { useState, useReducer } from 'react';
import ReactDOM from 'react-dom';

function reducer(state, action) {
  switch (action.type) {
    case 'up':
      return { count: state.count + 1 };
    case 'down':
      return { count: state.count - 1 };
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 1 })
  return (
    <div>
      {state.count}
      <button onClick={() => dispatch({ type: 'up' })}>+</button>
      <button onClick={() => dispatch({ type: 'down' })}>+</button>
    </div>
  );
}
ReactDOM.render(<Counter />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

复杂点逻辑:

const initialState = [
  { id: 1, name: "张三" },
  { id: 2, name: "李四" }
];

const reducer = (state: any, { type, payload }: any) => {
  switch (type) {
    case "add":
      return [...state, payload];
    case "remove":
      return state.filter((item: any) => item.id !== payload.id);
    case "update":
      return state.map((item: any) =>
        item.id === payload.id ? { ...item, ...payload } : item
      );
    case "clear":
      return [];
    default:
      throw new Error();
  }
};

const List = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      List: {JSON.stringify(state)}
      <button
        onClick={() =>
          dispatch({ type: "add", payload: { id: 3, name: "周五" } })
        }
      >
        add
      </button>
      <button onClick={() => dispatch({ type: "remove", payload: { id: 1 } })}>
        remove
      </button>
      <button
        onClick={() =>
          dispatch({ type: "update", payload: { id: 2, name: "李四-update" } })
        }
      >
        update
      </button>
      <button onClick={() => dispatch({ type: "clear" })}>clear</button>
    </>
  );
};
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

# useCallback

useCallback 可以理解为将函数进行了缓存,它接收一个回调函数和一个依赖数组,只有当依赖数组中的值发生改变时,该回调函数才会更新

function UseCallbackDemo() {
  const [count, setCount] = useState(0)

  const handleResize = useCallback(() => {
    console.log(`the current count is: ${count}`)
  }, [count])

  useEffect(() => {
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [handleResize])
  return (
    <div>
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        click
      </button>
      <p>current count: {count}</p>
    </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

该例子中,当改变 count 后,然后改变浏览器窗口大小,可以获取到最新的 count 。如果传入的依赖为 [],handleResize 不会更新,则改变浏览器窗口时, count 的值始终为 0 。

// 除非 `a` 或 `b` 改变,否则不会变
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
1
2
3
4
5
6
7

# useMemo

useMemo 对值进行了缓存,与 useCallback 类似,接收一个创建值的函数和一个依赖数组,它仅会在某个依赖项改变时才重新计算 memoized 值,这种优化有助于避免在每次渲染时都进行高开销的计算。

与 vue 的 computed 类似,主要是用来避免在每次渲染时都进行一些高开销的计算

useMemo 主要用于渲染过程优化,两个参数依次是计算函数(通常是组件函数)和依赖状态列表,当依赖的状态发生改变时,才会触发计算函数的执行。如果没有指定依赖,则每一次渲染过程都会执行该计算函数。

function UseMemoDemo() {
  const [count, setCount] = useState(0)
  const [value, setValue] = useState('')
  const useMemoChild = useMemo(() => <Child count={count} />, [count])
  return (
    <div>
      <p>{count}</p>
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        click
      </button>
      <br />
      <input value={value} onChange={e => setValue(e.target.value)} />
      {useMemoChild}
    </div>
  )
}
function Child({ count }: { count: number }) {
  console.log('child render')
  return (
    <Fragment>
      <p>useMemo hooks</p>
      <p>child count: {count}</p>
    </Fragment>
  )
}
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

该例子中,UseMemoDemo 组件引用了 Child 组件,在 UseMemoDemo 组件中,定义了 countvalue 两个 state,如果不使用 useMemo,那么每当 UseMemoDemo 中 input 发生改变时,Child 组件就会重新渲染。但 Child 组件 UI 只和 count 有关,那么这样就会造成 Child 组件无效更新,因此就引入了 useMemo,将 count 作为依赖传入,这样只有当 count 值发生改变时, Child 组件才会重新渲染。

不管页面 render 几次,时间戳都不会被改变,因为已经被被缓存了,除非依赖改变。

// ...
const getNumUseMemo = useMemo(() => {
  return `${+new Date()}`;
}, []);
// ...

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
import { useState, useMemo } from 'react';
import ReactDOM from 'react-dom';

function Time() {
  return <p>{Date.now()}</p>;
}

function Counter() {
  const [count, setCount] = useState(0);

  const memoizedChildComponent = useMemo((count) => {
    return <Time />;
  }, [count]);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
      <div>{memoizedChildComponent}</div>
    </div>
  );
}

ReactDOM.render(<Counter />, document.getElementById('root'));
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

# 父子组件更新优化

react中只要父组件的 render 了,那么默认情况下就会触发子组 的 render,react提供了来避免这种重渲染的性能开销的一些方法: React.PureComponentReact.memoshouldComponentUpdate()

Reace.memo只会对props做浅比较

React.momo其实并不是一个hook,它其实等价于PureComponent,但是它只会对比props。

const Index = () => {
  const [count, setCount] = useState(0);
  const getList = (n) => {
    return Array.apply(Array, Array(n)).map((item, i) => ({
      id: i,
      name: "张三" + i
    }));
  };
  return (
    <>
      <Child getList={getList} />
      <button onClick={() => setCount(count + 1)}>count+1</button>
    </>
  );
};
const Child = ({ getList }) => {
  console.log("child-render");
  return (
    <>
      {getList(10).map((item) => (
        <div key={item.id}>
          id:{item.id},name:{item.name}
        </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

来尝试解读一下,当点击“count+1”按钮,发生了这样子的事:父组件render > 子组件render > 子组件输出 "child-render"为了避免子组件做没必要的渲染,这里用了React.memo,如:

// ...
const Child = React.memo(({ getList }) => {
  console.log("child-render");
  return (
    <>
      {getList(10).map((item) => (
        <div key={item.id}>
          id:{item.id},name:{item.name}
        </div>
      ))}
    </>
  );
});
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们不假思索的认为,当我们点击“count+1”时,子组件不会再重渲染了。但现实是,还是依然会渲染,这是为什么呢? 答:Reace.memo只会对props做浅比较,也就是父组件重新render之后会传入 不同引用的方法 getList,浅比较之后不相等,导致子组件还是依然会渲染。

这时候,useCallback 就可以上场了,它可以缓存一个函数,当依赖没有改变的时候,会一直返回同一个引用。如:

// ...
const getList = useCallback((n) => {
  return Array.apply(Array, Array(n)).map((item, i) => ({
    id: i,
    name: "张三" + i
  }));
}, []);
// ...
1
2
3
4
5
6
7
8

总结:如果子组件接受了一个方法作为属性,在使用 React.memo 这种避免子组件做没必要的渲染时候,就需要用 useCallback 进行配合,否则 React.memo 将无意义。

# useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数 (initialValue)返回的 ref 对象在组件的整个生命周期内保持不变。在 function 组件中, 使用 useRef 主要可以完成以下两件事:

  • 保存变量
  • 获取 dom 结构; 用它来访问DOM,从而操作DOM

官方例子

function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`)
    }, 3000)
  })
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果我们 3s 点多次点击 button,那么控制台输出的结果会是 0,1,2,3…, 这是由于每次渲染时 count 的值都是固定的。但类似的逻辑在 class 组件中表现不一样

componentDidUpdate() {
  setTimeout(() => {
    console.log(`You clicked ${this.state.count} times`);
  }, 3000);
}
1
2
3
4
5

在 class 组件中,我们在 3s 内多次点击 button,最后在控制台输出的结果是最后一次 count 更新的值,那么如果使用 useRef 来实现这种效果? 前面说过 useRef 返回的对象在组件的整个生命周期内保持不变,它与自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象,那么我们可以这样处理:

function useRefDemo() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)
  useEffect(() => {
    countRef.current = count
    setTimeout(() => {
      console.log(`You clicked ${countRef.current} times`)
    }, 2000)
  }, [count])
  return (
    <div>
      <p>count: {count}</p>
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        click
      </button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

每次渲染时,将 count 的值赋值给 countRef.current由于 useRef 始终返回的是同一个对象,因此 countRef.current 始终是最新的 count 值,这种特性有点类似于 class 组件中的实例字段。

先看一个获取 dom 节点, 点击 button 时,input 聚焦。

function UseRefDemo() {
  const inputRef = useRef(null as any)
  const handleFocusInput = () => {
    inputRef.current.focus()
  }
  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleFocusInput}>click focus</button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12

注意:返回的 ref 对象在组件的整个生命周期内保持不变。 它类似于一个 class 的实例属性

# 跨组件操作

要访问的是一个组件,操作组件里的具体DOM我们就需要用到 React.forwardRef 这个高阶组件,来转发ref,如:

const Index = () => {
  const inputEl = useRef(null);
  const handleFocus = () => {
    inputEl.current.focus();
  };
  return (
    <>
    <Child ref={inputEl} />
    <button onClick={handleFocus}>Focus</button>
    </>
  );
};
const Child = forwardRef((props, ref) => {
  return <input ref={ref} />;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时,自定义暴露给父组件的实例值,可以让我们在父组件调用到子组件暴露出来的属性/方法;在大多数情况下,应当避免使用 ref 这样的命令式码。useImperativeHandle 应当与 forwardRef 一起使用

# 父组件引用子组件

function SamyInput(props, ref) {
  const inputRef = useRef(null as any)
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    }
  }))
  return <input ref={inputRef} />
}
const SamyInputRef = forwardRef(SamyInput)
const useImperativeHandleDemo = () => {
  const inputRef = useRef(null as any)
  useEffect(() => {
    inputRef.current.focus()
  })
  return <SamyInputRef ref={inputRef} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在 useImperativeHandleDemo 中,调用 inputRef.current.focus() 让 input 聚焦。

const Index = () => {
  const inputEl = useRef();
  useEffect(() => {
    console.log(inputEl.current.someValue);
    // test
  }, []);

  return (
    <>
      <Child ref={inputEl} />
      <button onClick={() => inputEl.current.setValues((val) => val + 1)}>
        累加子组件的value
      </button>
    </>
  );
};

const Child = forwardRef((props, ref) => {
  const inputRef = useRef();
  const [value, setValue] = useState(0);
  useImperativeHandle(ref, () => ({
    setValue,
    someValue: "test"
  }));
  return (
    <>
      <div>child-value:{value}</div>
      <input ref={inputRef} />
    </>
  );
});
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

总结:类似于vue在组件上用 ref 标志,然后 this.$refs.xxx 来操作dom或者调用子组件值/方法,只是react把它“用两个钩子来表示”。

# useLayoutEffect

在所有的 DOM 变更之后同步调用effect。可以使用它来读取 DOM 布局并同步 触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同 步刷新,也就是说它会阻塞浏览器绘制。所以尽可能使用 useEffect 以避免阻 塞视觉更新。

函数签名 (opens new window)useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。 看一个简单的例子:

const BlinkyRender = () => {
  const [value, setValue] = useState(0);
  useEffect(() => {
    if (value === 0) {
      setValue(10 + Math.random() * 200);
    }
  }, [value]);
  return (
    <div onClick={() => setValue(0)}>value: {value}</div>
  );
};
1
2
3
4
5
6
7
8
9
10
11

当我们快速点击时,value 会发生随机变化,但 useEffect 是 UI 已经渲染到屏幕上以后才会执行value 会先渲染为 0,然后在渲染成随机数,因此屏幕会出现闪烁。

useLayoutEffect 中修改 value 的值:

useLayoutEffect(() => {
  if (value === 0) {
    setValue(10 + Math.random() * 200);
  }
}, [value]);
1
2
3
4
5

相比使用 useEffect,当点击 div,value 更新为 0,此时页面并不会渲染,而是等待 useLayoutEffect 内部状态修改后,才会去更新页面,所以页面不会闪烁

# useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签,

function useFriendStatus() {
  const [isOnline] = useState(null)
  useDebugValue(isOnline ? 'Online' : 'Offline')
  return isOnline
}
const App = () => {
  const isOnline = useFriendStatus()
  return <div>{isOnline}</div>
}
1
2
3
4
5
6
7
8
9

在 React 开发者工具中会显示 “FriendStatus: Offline” 在某些情况下,格式化值的显示可能是一项开销很大的操作,因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查(打开 React 开发者工具)时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。 例如, 一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:

useDebugValue(date, date => date.toDateString());
1

# Hooks与setInterval

# 普通的定时功能

handleResize = () => {
  this.setState({
    reload: true,
  }, () => {
    this.setState({ reload: false });
  });
}
componentDidMount() {
  this.resizeHandler = _.debounce(this.handleResize, 500); // 防抖, 当窗口大小改变
  window.addEventListener('resize', this.resizeHandler);
  this.timer = setInterval(() => this.handleResize(), this.state.refreshTimeValue * 1000);
}
componentWillUnmount() {
  window.removeEventListener('resize', this.resizeHandler);
  this.timer && clearTimeout(this.timer);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# hooks定时实践

# useEffect方式简单

function Index() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(timer);
  }, [count]);
  return <div>{count}</div>;
}
1
2
3
4
5
6
7
8
9
10

# useCallback方式

function Index() {
  const [stationId, setStationId] = useState(1);
  const [startDate, setStartDate] = useState('2019-01-01');
  const [endDate, setEndDate] = useState('2019-01-10');
  
  const fetchData = useCallback(() => {
    axios.get('url', {
      params: {
        stationId,
        startDate,
        endDate,
      },
    })
      .then(res => console.log(res.data));
  }, [stationId, startDate, endDate]);

  useEffect(() => {
    const timer = setInterval(() => {
      fetchData();
    }, 1000);
    return () => clearInterval(timer);
  }, [fetchData]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# useRef方式及自定义hooks

useRef 并不仅仅是 React Hooks 版本的 createRef,它还具有一个非常有用的特性。在每次渲染中,useRef 所返回的值指向的都是同一个对象,而且该对象的 current 属性是可变的。说白了,这个 current 属性是一个可被直接修改、修改后可被直接读取且能够被传递到下次渲染的变量。

function Counter() {
  let [count, setCount] = useState(0);
  const myRef = useRef(null);
  myRef.current = () => {
    setCount(count + 1);
  };
  useEffect(() => {
    const id = setInterval(() => {
      myRef.current();
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>{count}</h1>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function useInterval(fun) {
  const myRef = useRef(null);
  useEffect(() => {
    myRef.current = fun;
  }, [fun]);
  useEffect(() => {
    const id = setInterval(() => {
      myRef.current();
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

function Counter() {
  let [count, setCount] = useState(0);
  useInterval(() => {
    setCount(count + 1);
  });
  return <h1>{count}</h1>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# useReducer处理复杂场景

虽然在简单场景中,使用 useReducer 会增加代码的体量,带来的不必要的开发量,但遇到复杂场景时,useReducer 的优点便凸显出来。

  • 一方面,使用 useReducer 后开发者可以用固有的思维模式去写 setInterval,即使遇到很复杂的页面逻辑也不容易出错;
  • 另一方面,在复杂场景下用 reducer 去管理状态,会使数据更新的过程变得清晰有条理,便于后期的维护。

count变量存入reducer中,使用useReducer更新count

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return state + 1;
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, 0);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: "increment" });
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>{state}</h1>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 实际实践

# useCallback缓存

const { rowId, time, loading, isTimerRefresh } = props;
const [data, setData] = useState({});
const fetchData = useCallback(
  () => {
    const { dispatch } = props;
    dispatch({
      type: `${moduleName}/getChartData`,
      payload: {
        chartId: rowId,
        startDate: (time && time.startTime) || '',
        endDate: (time && time.endTime) || '',
      },
    }).then(res => {
      if (res.isSuccess) {
        setData(res.data);
      }
    });
  },
  [rowId, time]
);
useEffect(
  () => {
    let timer;
    if (isTimerRefresh) {
      timer = setInterval(() => {
        fetchData();
      }, 5000);
    } else {
      fetchData(); // 加载一次
    }
    return () => clearInterval(timer);
  },
  [fetchData, isTimerRefresh]
);
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

# 自定义hooks/ useRef

function useInterval(callback, watch) {
  const ref = useRef();
  useEffect(
    () => {
      ref.current = callback;
    }
  );
  useEffect(
    () => {
      let timer;
      const [isTimerRefresh] = watch;
      if (isTimerRefresh) {
        timer = setInterval(() => {
          ref.current();
        }, 5000);
      } else {
        ref.current();
      }
      return () => clearInterval(timer);
    },
    watch ? [...watch] : [] // 判断是否有需要监测的属性
  );
}
const [data, setData] = useState({});
useInterval(
  () => {
    const { dispatch } = props;
    dispatch({
      type: `${moduleName}/getChartData`,
      payload: {
        chartId: rowId,
        startDate: (time && time.startTime) || '',
        endDate: (time && time.endTime) || '',
      },
    }).then(res => {
      if (res.isSuccess) {
        setData(res.data);
      }
    });
  },
  [isTimerRefresh, rowId, time]
);
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

# 增强 Hooks

由于每一个 Hooks API 都是纯函数的概念,使用时更关注输入 (input) 和输出 (output),因此可以更好的通过组装函数的方式,对不同特性的基础 Hooks API 进行组合,创造拥有新特性的 Hooks。

  • useState 维护组件状态
  • useEffect 处理副作用
  • useContext 监听 provider 更新变化

# useFetchHook

function useFetchHook(config, watch) {
  const [data, setData] = useState(null);
  const [status, setStatus] = useState(0);
  useEffect(
    () => {
      const fetchData = async () => {
        try {
          const result = await axios(config);
          setData(result.data);
          setStatus(1);
        } catch (err) {
          setStatus(2);
        }
      };

      fetchData();
    },
    watch ? [watch] : []
  );
  return { data, status };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# useInterval

/**
const timer = useInterval(()=>{ 
	console.log(`我是编号为${timer}的定时器`) 
},1000)
// 清除定时器
clearInterval(timer)
**/

export default function useInterval(callback,time=300){
  const intervalFn = useRef({}); 
  useEffect(()=>{
    intervalFn.current.callback = callback;  
  })
  useEffect(()=>{
    intervalFn.current.timer = setInterval(()=>{
      intervalFn.current()
    },time)
    return ()=>{ 
      intervalFn.current.timer && 
        clearInterval(intervalFn.current.timer) 
    }
  },[time])  
  return intervalFn.current.timer  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# useDidMount

import { useEffect } from 'react';
const useDidMount = fn => useEffect(() => fn && fn(), []);
export default useDidMount;
1
2
3

# useDidUpdate

import { useEffect, useRef } from 'react';
const useDidUpdate = (fn, conditions) => {
  const didMoutRef = useRef(false);
  useEffect(() => {
    if (!didMoutRef.current) {
      didMoutRef.current = true;
      return;
    }
    // Cleanup effects when fn returns a function
    return fn && fn();
  }, conditions);
};
export default useDidUpdate
1
2
3
4
5
6
7
8
9
10
11
12
13

# useWillUnmount

在讲到 useEffect 时已经提及过,其允许返回一个 cleanup 函数,组件在取消挂载时将会执行该 cleanup 函数,因此 useWillUnmount 也能轻松实现~

import { useEffect } from 'react';
const useWillUnmount = fn => useEffect(() => () => fn && fn(), []);
export default useWillUnmount;
1
2
3

# useHover

// lib/onHover.js
import { useState } from 'react';
const useHover = () => {
  const [hovered, set] = useState(false);
  return {
    hovered,
    bind: {
      onMouseEnter: () => set(true),
      onMouseLeave: () => set(false),
    },
  };
};
export default useHover;

import { useHover } from './lib/onHover.js';
function Hover() {
  const { hovered, bind } = useHover();
  return (
    <div>
      <div {...bind}>
        hovered:
        {String(hovered)}
      </div>
    </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

# useField

// lib/useField.js
import { useState } from 'react';
const useField = (initial) => {
  const [value, set] = useState(initial);
  return {
    value,
    set,
    reset: () => set(initial),
    bind: {
      value,
      onChange: e => set(e.target.value),
    },
  };
}
export default useField;

import { useField } from 'lib/useField';
function Input {
  const { value, bind } = useField('Type Here...');
  return (
    <div>
      input text:
      {value}
      <input type="text" {...bind} />
    </div>
  );
}
function Select() {
  const { value, bind } = useField('apple')
  return (
    <div>
      selected:
      {value}
      <select {...bind}>
        <option value="apple">apple</option>
        <option value="orange">orange</option>
      </select>
    </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

# useImgLazy

// 判断是否在视口里面
function isInWindow(el){
  const bound = el.getBoundingClientRect();
  const clientHeight = window.innerHeight;
  return bound.top <= clientHeight + 100;
}
// 加载图片真实链接
function loadImg(el){
  if(!el.src){
    const source = el.getAttribute('data-sourceSrc');
    el.src = source;
  }
}
// 加载图片
function checkImgs(className){
  const imgs = document.querySelectorAll(`img.${className}`);
  Array.from(imgs).forEach(el =>{
    if (isInWindow(el)){
      loadImg(el);
    }
  })
}
export default function useImgLazy(className){
  useEffect(()=>{
    window.addEventListener('scroll',()=>{
      checkImgs(className)
    });
    checkImgs(className);
    return ()=>{
      window.removeEventListener('scroll')
    }
  },[])
}
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
function App(){
  // ...
  useImgLazy('lazy-img')
  // ...
  return (
    <div>
      // ...
      <img className='lazy-img' data-sourceSrc='真实图片地址'/>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11

# 相关链接

https://juejin.cn/post/6844904165500518414

https://reactjs.org/docs/hooks-intro.html

https://reactjs.org/docs/hooks-reference.html

https://usehooks-typescript.com/

上次更新: 2022/04/15, 05:41:29
×