React Hooks
最近接触了hooks这个好东西,用了以后才发现是那么的好用,想想总结一下最近使用的一些思考和看法.我觉得hooks是一个趋势,用了hooks以后就回不去了的感觉
React Hooks介绍
hooks的出现使得原来要用类声明组件的方式变为函数式声明,原来有状态和无状态,现在一律都为无状态组件了.也让单元测试更加方便.正因为没有了类的声明方式,也就没有了生命周期.但是声明周期是我们一直以来在react非常重要的概念.不管是react还是vue.声明周期一直是很重要的一块知识.但是hooks它用另外一种思考方式帮助我们用更少的代码,更优雅的理念去实现我们的业务.换句话说,hooks也实现了生命周期,可能它做的更好.
Hooks到底哪好了?
首先,不说别的,它写的代码简洁
class写法:
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
num: 0
}
}
addNum = () => {
this.setState((prevState) => ({
num: prevState.num + 1
}))
}
render() {
return (
<div>
<p>{this.state.num}</p>
<button onClick={this.addNum}>Chlick me</button>
</div>
);
}
}
export default App;
再来看看hooks写法:
import React, { useState } from 'react';
const App = () => {
const [num, setNum] = useState(0)
return (
<div>
<p>{num}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
</div>
)
}
export default App;
告别了state,告别了生命周期,没有了class,最重要的是我们不用绑定this了!跟this基本就👋了
刚才介绍了一下hooks的基本使用,就是大伙见个面留个好印象,接下来我们看看useState是如何使用的
useState
useState是用来声明状态变量的
声明方式
const [num, setNum] = useState(0)
const [变量名, 修改变量名函数名] = useState(初始值)
通过ES6解构赋值,这个变量名和函数名是你随便取的哈,但是为了我们自己好辨认,函数名就都习惯性的加setFunc的方式写.
当然我们的变量可以声明多个
const [num1, setNum1] = useState(0)
const [num2, setNum2] = useState({age: 25})
const [num3, setNum3] = useState([1,2,3])
...
useState不仅仅接受基本类型和对象,数组,还可以传入一个函数,但是这个函数只执行一次.
import React, { useState } from 'react';
const App = ({price}) => {
const [num, setNum] = useState(() => price || 5)
return (
<div>
<p>{num}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
</div>
)
}
export default App;
这个获取就简单了,就直接把变量名放在JSX中就OK了,只不过不会像之前一样还有this.state.
<p>{num}</p>
修改变量
原来要通过setState,现在要用到我们刚才声明的setNum来修改
<button onClick={() => {setNum(num + 1)}}>Add</button>
此外useState每一次渲染都会记住上一次的值,因此如果我们想获取这次渲染前的值的时候,我们可以传入匿名函数来获取
<button onClick={() => {setNum((num) => num + 1)}}>Add</button>
最后一点说明,hooks不要在条件语句等环境下去使用hooks,因为声明useState的位置是一个数组,你改变了useState的顺序的时候,这个useState的数据就会出现混乱.导致报错
import React, { useState } from 'react';
const App = ({price}) => {
let num, setNum
if (Math.random > 0.5) {
[num, setNum] = useState(1)
} else {
[num, setNum] = useState(2)
}
return (
<div>
<p>{num}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
</div>
)
}
export default App;
useEffect
当数据产生变化useEffect,会执行一系列的副作用
其实useEffect可以当做componentDidMount, componentDidUpdate, componentWillUnmount生命周期的结合,但是,如果想用好useEffect还是要费一些功夫的.但是用好了你会发现useEffect确实很好用,一个useEffect三个生命周期全部搞定.
既然useEffect可以用来实现生命周期,那么就看看到底怎么实现生命周期
使用
import React, { useState, useEffect } from 'react';
const App = ({price}) => {
const [num, setNum] = useState(() => price || 5)
useEffect(() => {
console.log('num改变执行了useEffect')
})
return (
<div>
<p>{num}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
</div>
)
}
export default App;
useEffect 在第一次渲染和数据发生改变的时候就会执行一次,因此我们可以总结一下
useEffect此时相当于componentDidMount + componentDidUpdate
**
此时需注意一个问题,useEffect是异步的,因此,它不会阻碍页面的渲染视图,但是componentDidMount + componentDidUpdate是同步的.如果想测量宽高等布局的时候可以使用useLayoutEffect
下面我们来实现一下componentWillUnmount
想实现componentWillUnmount,必须要返回一个函数的方式来进行解绑.具体看代码
import React, { useState, useEffect } from 'react';
const App = () => {
const [num, setNum] = useState(0)
const [width, setWidth] = useState(document.body.clientWidth)
const onChangeSize = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
console.log('初次渲染和改变数据都会执行')
window.addEventListener('resize', onChangeSize)
return () => {
console.log('卸载组件和改变数据执行')
window.removeEventListener('resize', onChangeSize)
}
})
return (
<div>
<p>{width}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
</div>
)
}
export default App;
从代码中我们可以看到,通过返回一个函数我们可以去进行解绑操作,但是,如果你修改num的数据的时候return也会执行这里先详细说一下具体的useEffect执行顺序
- 页面渲染,执行 console.log('初次渲染和改变数据都会执行'), return 的函数不执行
- 改变数据后,先执行return 的函数 console.log('卸载组件和改变数据执行'),再执行console.log('初次渲染和改变数据都会执行')
我们可以看到,我们的需求实现是有问题的,我想在**componentDidMount **实现监听, componentWillUnmount 实现解绑,但是由于改变数据也进行了解绑操作,这是有问题的,因此需要useEffect的第二个函数
那么我们来实现一下上面的需求吧
import React, { useState, useEffect } from 'react';
const App = () => {
const [num, setNum] = useState(0)
const [width, setWidth] = useState(document.body.clientWidth)
const onChangeSize = () => {
setWidth(document.body.clientWidth)
}
// 实现需求的useEffect
useEffect(() => {
// componentDidMount
window.addEventListener('resize', onChangeSize)
return () => {
// componentWillUnmount
window.removeEventListener('resize', onChangeSize)
}
}, [])
useEffect(() => {
// componentDidMount + componentDidUpdate
document.title = num
})
useEffect(() => {
// componentDidMount
setTimeout(() => {
console.log('ajax请求')
}, 1000)
}, [])
useEffect(() => {
console.log(`num改变为${num}`)
}, [num])
return (
<div>
<p>{width}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
</div>
)
}
export default App;
传入空数组的意思是,这个useEffect已经和数据无关了.
其实这个数组是用来告诉useEffect到底被谁影响,既然你写了空数组,它就和任何state数据无关了.如果想关联上state数,上面代码的第33行明确指出了,只有num才能影响其useEffect.你也写多个state数据,去关联这个useEffect.
useEffect是可以写多个的.不会像之前的声明周期一样要都写在一起,做很多的判断了
下面我们做一下总结
useEffect的第二个参数有六种情况
- 没有第二个参数,这是最简单的,不写return函数,就相当于 componentDidMount + componentDidUpdate
- 没有第二个参数,写return函数,就相当于 componentDidMount + componentDidUpdate + 不严谨的componentWillUnmount, 但是由于改变数据会执行return的函数产生干扰,因此没有实现真正意义上的三种生命周期
- 只传入一个空数组[], 不写return函数,那么它只会调用一次,它的含义就告诉我们这个useEffect和数据已经没有关系了.但是第一次渲染的时候会执行,相当于 componentDidMount
- 只传入一个空数组[], 写return函数,和第三种方式相同,唯一不同的是由于return 函数,在卸载组件的时候也会执行,相当于 componentDidMount + componentWillUnmount
- 传入一个数组,其中包括变量时,没有return函数,只有数组中的变量改变了,useEffect才会执行
- 传入一个数组,其中包括变量时,又有return函数,那么它相当于 componentDidMount + 特定的componentDidUpdate + 不严谨的componentWillUnmount,因为它又因为数据去执行return函数了
因此.如果想实现生命周期的作用采用1,3,4. 第二种不做推荐,逻辑比较混乱,第五种可以在你做一些逻辑需求的时候可以使用,第六种的话我没怎么尝试过,因为我尽量都拆分着写.
其实我认为没有必要去写的那么复杂,能拆分的还是拆分吧
useContext
帮助我们获取跨层级组件传递变量
下面说一个🌰:
首先,我们有这样一个情景
爷爷组件App, 儿子组件Detail,孙子组件Btn
爷爷有个变量传给孙子,但是中间隔着儿子,传递很麻烦,一般我们传递给儿子就需要props就可以了.但是明显如果爷爷年龄比较大,还有重孙,那么我们岂不是更麻烦了.这个时候可以用到context了
PS: context和redux所解决的不是一个事情,一个是解决传值,一个是解决全局数据管理
// 爷爷组件 App.js
import React, { useState, createContext } from 'react';
import Detail from './Detail'
const NumContext = createContext()
const App = () => {
const [num, setNum] = useState(0)
return (
<div>
<p>{num}</p>
<button onClick={() => {setNum(num + 1)}}>Add</button>
<NumContext.Provider value={num}>
<Detail />
</NumContext.Provider>
</div>
)
}
export {App, NumContext};
// 儿子孙子组件 Detail Btn
import React, {useContext} from 'react'
import { NumContext } from './App'
const Detail = () => {
return <div id="Detail"><Btn /></div>
}
const Btn = () => {
const num = useContext(NumContext)
return <button>{num}</button>
}
export default Detail
想实现context总共分
- 创建createContext
const NumContext = createContext()
- 利用创建好得到的组件我们写一个闭合标签, value是你想提供的变量
<NumContext.Provider value={num}>
// 组件..
</NumContext.Provider>
- 在孙子组件引入你在某某长辈那创建的context组件,再使用我们的hooks来获取到 value值
import React, {useContext} from 'react'
import { NumContext } from './App'
const Btn = () => {
const num = useContext(NumContext)
return <button>{num}</button>
}
由此我们可以发现,不管多少层级,都是可以获取到的
useReducer
useReducer其实就是模拟了Redux的Reducer,而且它经常和useContext结合使用,起到 Redux的效果
👇看🌰:
import React, { useReducer } from 'react';
const App = () => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'increase':
return {num: state.num + 1}
case 'decrease':
return {num: state.num - 1}
default:
return state
}
}, {
num: 0
})
return (
<div>
<p>{state.num}</p>
<button onClick={() => {dispatch({type: 'increase'})}}>increase</button>
<button onClick={() => {dispatch({type: 'decrease'})}}>decrease</button>
</div>
)
}
export default App;
第一个参数就是一个reducer函数,第二个参数是默认state值,它可以返回两个值,一个是state,另一个就是dispatch
useContext UseReducer 实现 Redux
下面来简单分享一下我的一写理解, 利用hooks特性实现 Redux 相似的功能
目录如下:

其实跟之前写法差不多,稍微有那么点去区别
首先是index - 这个文件是根文件,我们在整个项目中让所有组件都能获取store中的数据:
// index.js
import React from 'react'
import {Color} from './store/state'
import Home from './components/Home'
const App = () => {
return (
<div>
<Color>
{/* 里面包含所有的子组件, 这里以 Home 为例 */}
<Home />
</Color>
</div>
)
}
export default App
👇看store中的内容,其实上面已经引入了store中文件了
// state.js
import React, { createContext, useReducer } from 'react'
import {reducer} from './reducer'
// 1. 默认 state
const defaultState = {
color: 'blue'
}
// 2. 创建一个 context, 作用是让所有受包裹的组件都能够得到 state, dispatch
export const DataContext = createContext()
export const Color = props => {
// 3. useReducer 创建出 state, dispatch, 将其放入 Provider 的 value 中
const [state, dispatch] = useReducer(reducer, defaultState)
return (
<DataContext.Provider value={{state, dispatch}}>
{props.children}
</DataContext.Provider>
)
}
此时已经成功一大半了.因为我们已经把state, dispatch 全部放入 value 中, 那么所有组件都已经可以全局的拿到 state了
下面就简单了.
// reducer.js
import { UPDATE_COLOR } from './constants'
export const reducer = (state, action) => {
switch (action.type) {
case UPDATE_COLOR:
return {color: action.color}
default:
return state
}
}
// constants.js
export const UPDATE_COLOR = 'UPDATE_COLOR'
// actionCreators.js
import { UPDATE_COLOR } from './constants'
export const updateColor = (color) => ({
type: UPDATE_COLOR,
color
})
至此我们已经完成了
useMemo
useMemo 的用处在于可以帮助我们节约资源
先举个🌰:
import React, { useState, useMemo } from 'react';
const App = () => {
const [a, setA] = useState('a')
const [b, setB] = useState('b')
return (
<div>
<p>{a} App</p>
<p>{b} App</p>
<button onClick={() => {setA(a + a)}}>aaa</button>
<button onClick={() => {setB(b + b)}}>bbb</button>
<Children theA={a}>{b}</Children>
</div>
)
}
const Children = ({theA, children}) => {
console.log('children 重新渲染')
const aChange = (getA) => {
console.log('useMemo')
return getA + ' useMemo'
}
return (
<div>
<p>{aChange()}</p>
<p>{children}</p>
</div>
)
}
export default App;
我们发现,如果不使用useMemo,在你改变 b 的值的时候, aChange 也会执行,也就是说,只要你改变父组件的任何变量,都会影响 Children. a 变量其实也没有发生任何变化,但是 aChange 依然执行了
因此我们需要使用 useMemo 控制一下
import React, { useState, useMemo } from 'react';
const App = () => {
const [a, setA] = useState('a')
const [b, setB] = useState('b')
return (
<div>
<p>{a} App</p>
<p>{b} App</p>
<button onClick={() => {setA(a + a)}}>aaa</button>
<button onClick={() => {setB(b + b)}}>bbb</button>
<Children theA={a}>{b}</Children>
</div>
)
}
const Children = ({theA, children}) => {
console.log('children 重新渲染')
const aChange = (getA) => {
console.log('useMemo')
return getA + ' useMemo'
}
const aVal = useMemo(() => aChange(theA), [theA])
return (
<div>
<p>{aVal}</p>
<p>{children}</p>
</div>
)
}
export default App;
使用 useMemo, 第一个为你所去计算值的函数,第二个参数为数组中的变量值的变化将会执行 useMemo 的函数
useRef
useRef可以帮助我们一个获得一个整个生命周期不变的对象
import React, { useState, useRef } from 'react';
const App = () => {
let [num, setNum] = useState(0);
return (
<div>
<Children />
<button onClick={() => setNum({ num: num + 1 })}>+</button>
</div>
)
}
let input;
function Children() {
const inputRef = useRef()
console.log(input === inputRef)
input = inputRef
return <input type="text" ref={inputRef} />
}
export default App;
自定义 Hook
自定义 hook 有点像我们写的函数, 但是自定义 hook 有自己的 state, 它只是帮助我们实现复用逻辑,但是它每次调用所得到的状态都是它自身.各个自定义 hook 之间的 state 相互无关 此外,自定义 hook 返回的结果的变化也会重新 render 父组件 在命名方面,我们通常用use做为自定义hook的命名方式,当然,这是一种不成文的规定
import React, { useState, useEffect } from 'react';
const useEnter = (key) => {
const [hasPressed, setHasPressed] = useState(false)
const keyDown = ({keyCode}) => {
if (key === keyCode) {
setHasPressed(true)
}
}
const keyUp = ({keyCode}) => {
if (key === keyCode) {
setHasPressed(false)
}
}
useEffect(() => {
// console.log('addEventListener')
document.addEventListener('keydown', keyDown)
document.addEventListener('keyup', keyUp)
return () => {
// console.log('removeEventListener')
document.removeEventListener('keydown', keyDown)
document.removeEventListener('keyup', keyUp)
}
})
return hasPressed
}
const App = () => {
console.log('render')
const [name, setName] = useState('kun')
const isEnter = useEnter(13)
useEffect(() => {
if (isEnter) {
setName('flower')
} else {
setName('kun')
}
}, [isEnter])
return (
<div>
<p style={{fontSize: '50px'}}>{name}</p>
</div>
)
}
export default App;