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
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>
)
}
}
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}/>)
}
}
}
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})
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})
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)
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}
}
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>
)
}
2
3
4
5
6
SetTimeComponent:计时组件
export default function SetTimeComponent (){
const {count} = useLogTime({time:true})
return(
<div>
<div>我是计时组件</div>
<span>{`我显示了${count}s`}</span>
</div>
)
}
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>
)
}
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'));
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'));
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
(opens new window)【维护状态】useEffect
(opens new window)【完成副作用操作】useContext
(opens new window)【使用共享状态】
- Additional Hooks
useReducer
(opens new window)【类似redux】useCallback
(opens new window)【缓存函数】useMemo
(opens new window)【缓存值】useRef
(opens new window)【访问DOM】useImperativeHandle
(opens new window)【使用子组件暴露的值/方法】useLayoutEffect
(opens new window)【完成副作用操作,会阻塞浏览器绘制】useDebugValue
(opens new window)【开发者工具中显示自定义 hook 的标签】
# 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>
</>
);
};
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>
}
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 ]
);
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 ]);
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();
};
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)
2
然后在 useContext
hook 使用 context
function UseContextDemo() {
const local = useContext(ThemeContext)
return (
<div>
<p>local: {local}</p>
</div>
)
}
// render: 🇨🇳
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: 🇺🇸
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>
</>
);
};
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'));
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'));
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
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>
);
}
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);
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'));
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
useReducer
是 useState
的一种代替方案,用于 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>
)
}
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>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在副作用中,我们执行 increase
先将 count
的值增加 1,然后执行 setFrozen
将 count
的值 “冻住”,再执行 increase
将 count
的值增加 1,由于在 setCount
进行了判断,如果 frozen
为 true
,则直接返回,否则增加 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
值依然为 false
, setCount
返回的 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>
}
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
后,将 count
和 frozen
关联起来,执行 dispath({ type: SET_FROZEN, frozen: true })
修改了 frozen
值 ,紧接着执行 dispath({ type: INCREASE })
时,此时获取到的 frozen
值为 true
,当然最后 render
结果就为 1 。
前面说了 useReducer
是 useState
的一种代替方案,那么如何使用 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
};
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
useReducer
和 useState
相比,优势在于可以将使用 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'));
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>
</>
);
};
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>
)
}
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],
);
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>
)
}
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 组件中,定义了 count
和 value
两个 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'));
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.PureComponent
、React.memo
,shouldComponentUpdate()
;
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>
))}
</>
);
};
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>
))}
</>
);
});
// ...
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
}));
}, []);
// ...
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>
)
}
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);
}
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>
)
}
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>
)
}
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} />;
});
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} />
}
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} />
</>
);
});
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>
);
};
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]);
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>
}
2
3
4
5
6
7
8
9
在 React 开发者工具中会显示 “FriendStatus: Offline”
在某些情况下,格式化值的显示可能是一项开销很大的操作,因此,useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查(打开 React 开发者工具)时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
例如, 一个返回 Date
值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString
函数调用:
useDebugValue(date, date => date.toDateString());
# 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);
}
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>;
}
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]);
}
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>;
}
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>;
}
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>;
}
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]
);
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]
);
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 };
}
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
}
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;
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
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;
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>
);
}
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>
);
}
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')
}
},[])
}
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>
)
}
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/