React Hook 在 react 16.8及以后的版本
中才会有
React Hook 解决的问题
1. 组件之间复用状态逻辑
2. 减少组件的复杂程度
在传统的 class 中,会使用 componentDidMount 和 componentDidUpdate 获取数据。同时 componentDidMount 中也会处理一些其他的事务,例如事件监听,定时器等等。而后还需要在 componentWillUnmount 中取消。万一忘记其中某一个部分或者处理的时间过多,很可能导致一些可怕的bug。
3. 关于 class 类与函数组件 this 的问题
对于一部分人来说,理解 class 中的 this 会比理解函数组件中的 this 更加困难,而且增加了学习成本。但是,react 中并不会移除 class 这种方法
State Hook
传统的 React 组件的 state 都是这样的,创建一个 state 与更新(this.setState)1
2
3
4
5
6
7
8
9
10
11
12
13
14import React from 'react';
class Demo extends React.Component{
constructor(props){
super(props)
this.state = {
count: 0
}
}
render(){
return (<div onClick={_ => this.setState({count: this.state.count++})}>{this.state.count}</div>)
}
}
使用 React Hook 后1
2
3
4
5
6import React, { useState } from 'react'
function Demo(){
let [count, setCount] = useState(0)
return (<div onClick={_ => setCount(count++)}>{count}</div>)
}
可以看到。使用 Hook 后的代码简洁了很多。但是,使用 useState
不会把新的 state 和旧的 state 进行合并。
上面,我们只是用了一个 count。但是通常一个组件都不会只有一个 state 的,这时候可以多次使用 useState
。
同时,定义 state 的时候定义在一个数组里面,可以猜到, useState 返回的不是一个不同的数字或者字符串,而是一个对象(数组)。这里这样定义,使用了 ES6 中的解构赋值
Effect Hook
useState
其实不难理解,唯一需要注意的就是 this.setState
是修改后的 state 与之前的 state 对比合并,而采用 useState
则是直接替换。
作为使用过一段时间的 React Hook 的程序员,个人认为 Effect Hook
才需要更多的理解。
React官方文档中这样定义的
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
所以,我们使用 Hook 后,数据获取、订阅或者手动修改过 DOM等都需要在 useEffect
中进行了。
不要以为
useEffect
和componentDidMount
、componentDidUpdate
和componentWillUnmount
一样只能使用一次,他与useState
一样,可以多次使用。
默认情况下,React 会在每次渲染后调用副作用函数(useEffect
) —— 包括第一次渲染的时候。所以,在 useEffect
函数中可以直接使用 props 和 state
useEffect
接收两个参数。第一个参数是一个函数,第一个参数相当于 componentDidMount
和 componentDidUpdate
,第一个参数可以有一个返回值(一般就是一个函数,我们将之称为清除函数),相当于与 componentWillUnmount
。这样一说,你可能就理解了。再来举个例子,更形象的说明一下
1 | class Demo extends React.Component { |
上面的是传统的方式,添加以及移除定时器的操作。因为需要在 componentWillUnmount
中进行判断,有时候(大部分时候)可能都会遗忘。
再来看看使用 useEffect
的代码1
2
3
4
5
6
7
8
9
10
11
12function Demo(){
let timer = null
useEffect(() => {
timer = setInterval(() => doSomething(), 1000)
// return 一个函数,将会在组件将要卸载的时候调用 相当于 componentWillUnmount
return () => clearInterval(timer)
})
return ...
}
可以看出,使用 useEffect
不单单是代码更简洁,同时使我们的代码逻辑看起来更直观。设置定时器与清除定时器是放在一个API里面的,代码的耦合更高。更能体现这是一个整体,也避免了遗忘。
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。
如果不涉及到异步,订阅等操作,可以不用返回清除函数
上面只是 useEffect
的一个简单的事例,它的功能不止于此。因为之前还说过,处理数据请求也是在里面处理的。那么怎么使用呢
1 | function Demo(){ |
如果只是上面那样写,会有一个严重的问题。之前说过, useEffect
是会在DOM初次加载完成以及DOM更新完成的时候调用,所以上面的请求会在每一次DOM更新的时候再次执行,而如果请求返回的结果会使DOM更新,那么,这就是一个无限循环的过程了。
那么怎么处理这个副作用呢?这时候就需要 useEffect
的第二个参数了。一般是一个数组
如果两次需要更新的数据没有变化,只需要在第二个参数(数组)中添加对应的变量,例如1
2
3useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
但是如果是上面的处理 ajax request
的 effect 。只需要传递一个空数组即可。这样,这个 effect 只会执行一次。
React 会对数组中的数据进行更新前后数据的对比,如果没有变化,那么则不更新
这个方法对于有清除函数的 effect 同样适用。
React官网中说到:未来版本,可能会在构建时自动添加第二个参数。期待他的到来,这将大大减少可能出现的bug。
其他 Hook
除了 useState
和 useEffect
两个常用的 Hook, 还有一些其他的 Hook, 这些可能用的不多。
useContext
1 | const value = useContext(MyContext); |
这个 Hook 用于连接 React 上下文。使用过 React.createContext
的老铁应该知道,这是创建一个 React 上下文
1 | const Context = React.createContext; |
使用 useContext
1
2
3const Context = React.createContext;
useContext(Context)
例子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
39const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 通过 useContext 使用 React.createContext(themes.light) 创建的 Context
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useReducer
1 | const [state, dispatch] = useReducer(reducer, initialArg, init); |
useState
的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
1 | const initialState = {count: 0}; |
既然作用类似于 Redux, 那么可以用这个取代 Redux 么?答案是可以的,不过需要结合 useContext
来使用。掘金上面有码友给出了一个例子用 useContext + useReducer 替代 redux。
你可以在新项目中或者涉及状态管理不多的项目中尝试使用,现有的大型项目不建议重构,使用 Redux 依然是不错的方案。
useCallback
1 | const memoizedCallback = useCallback( |
返回一个 memoized
回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
useMemo
1 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
返回一个 memoized 值。
useCallback
与 useMemo
都可以用于 React 性能优化的手段。
useRef
1 | const refContainer = useRef(initialValue); |
useRef
返回一个可变的 ref
对象,其 .current
属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
所以,这个方法就相当于 class 中的 ref
属性,用于获取具体的DOM元素。
1 | function TextInputWithFocusButton() { |
useImperativeHandle
useLayoutEffect
useDebugValue
上面未说明的 Hook 可以查看 React 官网
Hook 规则
Hook 永远是在最顶层调用,不能在条件判断语句或者其他语句中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Demo(){
// 正确
useEffect(() => {
if(name === 'tal'){
// do something
}
})
// 错误
if (name === 'tal') {
useEffect(() => {
// do something
})
}
}
如果你害怕你写错了,但是没有检查出来,可以使用 eslint-plugin-react-hooks 这个插件来检测。
自定义 Hook
Hook 我们也是可以自定义的。那么为什么需要自定义。答案是 逻辑共享。
假如有一个 state 需要在多个组件中使用,我们不应该在多个组件中都单独的去创建这个 state, 而是应该逻辑共享。把这个 state 以及操作这个 state 的方法定义在我们自己的 Hook 中。那这个 Hook 就是我们自定义的 Hook,其实,他也是一个函数,接收参数,返回你需要的值。唯一需要注意的是:自定义 Hook 必须以 use
开头,不管怎么变,使用需要遵循 React Hook 以 use
开头的规则。
下面是一个获取window视窗的hook
1 | import { useState, useCallback, useEffect } from 'react'; |