🎙️前言
相传, react 17
出了一个很强大的功能, 也就是 react hooks
。实际上, react hooks
有点类似与 vue3
的 composition API
,都是为了提升开发效率而出现。
那么,在下面的文章中,将从 0到1
开始,带大家了解 react hooks
,以及一些常用的 API
。
废话不多说,开始 react-hook
之旅叭~
一、📻概述
1、关于React Hooks
React Hooks
是一个可选功能,通常用class
组件 来和它做比较;100%
向后兼容,没有破坏性改动;- 不会取代
class
组件,尚无计划要移除class
组件。
2、认识React Hooks
(1)回顾React函数式组件
我们先来回顾下 class 组件 和 函数组件,具体如下:
class组件:
// class组件
class List extends React.Component {
constructor() {
super(props)
}
render() {
const {
List } = this.props
return <ul>{
list.map((item, index) => {
return <li key={
item.id}>
<span>{
item.title}</span>
</li>
})}</ul>
}
}
函数组件:
// 函数组件
function List(props) {
const {
list } = props
return <ul>{
list.map((item, index) => {
return <li key={
item.id}>
<span>{
item.title}</span>
</li>
})}</ul>
}
(2)函数组件的特点
函数组件的特点是:
- 没有组件实例;
- 没有生命周期;
- 没有
state
和setState
,只能接收props
。
(3)class组件的问题
上面我们说到了函数组件是一个纯函数,只能接收 props
,没有任何其他功能。而 class
组件拥有以上功能,但是呢,class
组件会存在以下问题:
- 大型组件很难拆分和重构,很难测试(即
class
不易拆分); - 相同业务逻辑,分散到各个方法中,逻辑混乱;
- 复用逻辑变得复杂,如
Mixins
、HOC
、Render Props
。
因此,有了以上问题的出现,也就有了 React Hooks
。
(4)React 组件
React
组件更易于用函数来表达:React
提倡函数式编程,即view=fn(props)
;函数更灵活,更易拆分,更易测试;
但函数组件太简单,需要增强能力 ——因此,有了
React Hooks
。
二、🪕几种 Hooks
1、State Hook🗞️
(1)让函数组件实现state和setState
- 默认函数组件是没有
state
的; - 函数组件是一个纯函数,执行完即销毁,无法存储
store
; - 需要
State Hook
,即把store
功能 “钩” 到纯函数中。
(2)举例阐述
假设我们现在要通过点击按钮,来修改设定的某个值。现在我们用 hooks
中的 useState
来处理。具体代码如下:
import React, {
useState } from 'react'
function ClickCounter() {
// 数组的解构
// useState 就是一个 Hook “钩”,最基本的一个 Hook
const [count, setCount] = useState(0) // 传入一个初始值,初始值可以是数值/字符串/数组/对象等类型
const [name, setName] = useState('星期一研究室')
// const arr = useState(0)
// const count = arr[0]
// const setCount = arr[1]
function clickHandler() {
setCount(count + 1)
setName(name + '2021')
}
return <div>
<p>你点击了 {
count} 次 {
name}</p>
<button onClick={
clickHandler}>点击</button>
</div>
}
export default ClickCounter
此时浏览器的显示效果如下:
在上面的代码中, count
是一个 state
的值, setCount
是修改 state
的一个函数。同理, name
也是一个 state
的值, setName
是修改 name
值得一个函数。
如果使用 hooks
来修改 state
值的话,那么我们只需要以 const [count, setCount] = useState(0)
这种形式去进行,便可修改最后的值,而不需要像 class
组件那么复杂的去使用。
对于上面这个功能来说,如果使用 class
组件来实现的话,具体代码如下:
import React from 'react'
class ClickCounter extends React.Component {
constructor() {
super()
// 定义 state
this.state = {
count: 0,
name: '星期一研究室'
}
this.clickHandler = this.clickHandler.bind(this)
}
render() {
return <div>
<p>你点击了 {
this.state.count} 次 {
this.state.name}</p>
<button onClick={
this.clickHandler}>点击</button>
</div>
}
clickHandler() {
// 修改 state
this.setState({
count: this.state.count + 1,
name: this.state.name + '2021'
})
}
}
export default ClickCounter
大家可以看到,如果使用 class
组件来解决的话,那么需要先定义 state
,然后再定义一个函数,再使用 setState
才能去修改值,这样似乎还麻烦了点。
相信到这里,大家已经感受到 hooks
的快乐之处了。
下面,我们来总结关于 useState
的一些知识点。
(3)useState使用总结
useState(xxx)
传入初始值,返回数组[state, setState]
;- 通过
state
获取值; - 通过
setState(xxx)
修改值。
(4)Hooks命名规范
- 规定所有的
Hooks
都要使用use
开头,如useXxx
; - 自定义
Hook
也要以use
开头; - 非
Hooks
的地方,尽量不要使用useXxx
写法,不然容易造成误解。
2、Effect Hook🗞️
(1)让函数组件模拟生命周期
- 默认函数组件没有生命周期;
- 函数组件是一个纯函数,执行完即销毁,自己无法实现生命周期;
- 使用
Effect Hook
把生命周期 “钩” 到纯函数中。
(2)举例阐述
同样地,我们用于 useState
相同的例子,来体验下 useEffect
。
我们先在 src|components
下建立一个文件,命名为 LiftCycles.js
。 具体代码如下:
import React, {
useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 模拟 class 组件的 DidMount 和 DidUpdate
useEffect(() => {
console.log('在此发送一个 ajax 请求')
})
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你点击了 {
count} 次 {
name}</p>
<button onClick={
clickHandler}>点击</button>
</div>
}
export default LifeCycles
现在,我们在 App.js
中注册该组件。具体代码如下:
import React, {
useState } from 'react';
import LifeCycles from './components/LifeCycles'
function App() {
const [flag, setFlag] = useState(true)
return (
<div
{
flag && <LifeCycles/>}
</div>
);
}
export default App;
此时,浏览器的显示效果如下:
大家可以看到, useEffect
如果只传入一个函数的话,那么它模拟的是 DidMount
和 DidUpdate
这两个生命周期的功能。每当我们点击一次的时候,浏览器就会打印一次,也就是组件加载和组件更新是在一起进行的。
那如果我们想要让组件加载和组件更新分开实现呢,同样有办法。我们来改造下 LiftCycles.js
的代码。具体代码如下:
import React, {
useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 模拟 class 组件的 DidMount
useEffect(() => {
console.log('加载完了')
}, []) // 第二个参数是 [] (不依赖于任何 state)
// 模拟 class 组件的 DidUpdate
useEffect(() => {
console.log('更新了')
}, [count, name]) // 第二个参数就是依赖的 state
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你点击了 {
count} 次 {
name}</p>
<button onClick={
clickHandler}>点击</button>
</div>
}
export default LifeCycles
此时浏览器的显示效果如下:
大家可以看到,第一次的时候分别加载和更新了 1
次。等到我们去点击的时候,因为已经加载完了,所以这个时候就是更新了。
那我们来梳理一下, useEffect
在加载和更新的时候,分别是怎么进行处理的?
看上面的代码中我们可以发现,useEffect
还可以接收第二个参数,第二个参数如果空值,那么它不依赖于任何 state
,表示 componentDidMount
这个生命周期。相反,如果第二个参数没有依赖值或者接收依赖于 state 的值这两种情况时,那么它模拟 componentDidUpdate
这个生命周期。
说到这里,可能有的小伙伴还想问,那跟销毁相关的生命周期,又怎么处理呢?我们继续来对 LiftCycles.js
的代码进行改造。具体如下:
import React, {
useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 模拟 class 组件的 DidMount
useEffect(() => {
let timerId = window.setInterval(() => {
console.log(Date.now())
}, 1000)
// 返回一个函数
// 模拟 WillUnMount
return () => {
window.clearInterval(timerId)
}
}, [])
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你点击了 {
count} 次 {
name}</p>
<button onClick={
clickHandler}>点击</button>
</div>
}
export default LifeCycles
大家可以看以上代码,在这里,我们通过 return
一个函数,去模拟 componentWillUnMount
这个生命周期,来销毁每一次定时器中执行的任务。
到这里,相信大家对 useEffect
已经有了一定的了解。现在,我们来对 useEffect
做个小结。
(3)useEffect使用总结
- 模拟
componentDidMount - useEffect
依赖[]
; - 模拟
componentDidUpdate - useEffect
无依赖,或者依赖[a, b]
; - 模拟
componentWillUnMount - useEffect
中返回一个函数。
(4)useEffect让纯函数有了副作用
- 默认情况下,执行纯函数的时候,只需要输入参数并返回结果即可,是没有任何副作用的;
- 所谓副作用,就是对函数之外造成影响,如设置全局定时任务;
- 而组件需要副作用,所以需要
useEffect
“钩” 到纯函数中; - 因此,
useEffect
这个副作用并不是一件坏事。
(5)useEffect中返回函数 fn
有一种值得注意的情况是,在上面我们通过返回一个函数来模拟 WillUnMount
,但这个模拟后的结果还不完全等于 WillUnMount
。现在,我们在 src|components
下建立一个文件,命名为 FriendStatus.js
。具体代码如下:
import React, {
useState, useEffect } from 'react'
function FriendStatus({
friendId }) {
const [status, setStatus] = useState(false)
// DidMount 和 DidUpdate
useEffect(() => {
console.log(`开始监听 ${
friendId} 在线状态`)
// 【特别注意】
// 此处并不完全等同于 WillUnMount
// props 发生变化,即更新,也会执行结束监听
// 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
return () => {
console.log(`结束监听 ${
friendId} 在线状态`)
}
})
return <div>
好友 {
friendId} 在线状态:{
status.toString()}
</div>
}
export default FriendStatus
App.js
的代码如下:
import React, {
useState } from 'react';
import FriendStatus from './components/FriendStatus'
function App() {
const [flag, setFlag] = useState(true)
const [id, setId] = useState(1)
return (
<div>
<div>
<button onClick={
() => setFlag(false)}>flag = false</button>
<button onClick={
() => setId(id + 1)}>id++</button>
</div>
{
flag && <FriendStatus friendId={
id}/>}
</div>
);
}
export default App;
此时,浏览器的显示效果如下:
大家可以看到,当开始下一个监听时,会先结束掉上一个监听再开始下一个。正如图中所看到的,一开始我们正在监听 id
为 1
的好友,那当我们想要点击 id++
按钮去监听好友 2
时,useEffect
会先去结束掉 1
的状态,再让好友 2
上线。
此时,我们要注意的一个点是,执行结束监听的这个函数,执行的是 DidUpdate
生命周期,而不是 WillUnMount
生命周期。函数在这个时候是属于更新状态而不是销毁状态。
依据以上内容,我们来做个小结。具体如下:
- 当
useEffect
依赖于[]
的时候,在组件销毁时执行return
的函数fn
,等于WillUnMount
生命周期; - 当
useEffect
无依赖或依赖于[a, b]
的时候,组件是在更新时执行return
的函数fn
;即在下一次执行useEffect
之前,就会先执行fn
,此时模拟的是DidUpdate
生命周期。
3、其他Hooks🗞️
上面我们讲了两个比较常用的 hooks
→ useState
和 useEffect
。接下来,我们来了解其他几个比较不常用的 hooks
。
(1)useRef
Ref
是用于对 值的修改 和 DOM 元素的获取 。先来看一段代码:
import React, {
useRef, useEffect } from 'react'
function UseRef() {
const btnRef = useRef(null) // 初始值
const numRef = useRef(0)
numRef.current = 2;
useEffect(() => {
console.log(btnRef.current) // DOM 节点
console.log(numRef.current); // 得到值
}, [])
return <div>
<button ref={
btnRef}>click</button>
</div>
}
export default UseRef
此时,浏览器的显示效果为:
大家可以看到,如果将 btnRef
绑定到 ref={btnRef}
上,那么 btnRef.current
获取到的是当前的 DOM
节点。
另外一种情况是,大家定位到 numRef
,如果我们给其进行赋值并修改,那么 numRef.current
最终得到的值是修改后的值。
(2)useContext
很多时候,我们经常会有一些比较静态的属性需要做切换,比如:主题颜色切换。这个时候就需要用到 context
上下文来处理。那在 hook
中,就有 useContext
可以处理这件事情。先来看一段演示代码:
import React, {
useContext } from 'react'
// 主题颜色
const themes = {
light: {
foreground: '#000',
background: '#eee'
},
dark: {
foreground: '#fff',
background: '#222'
}
}
// 创建 Context
const ThemeContext = React.createContext(themes.light) // 初始值
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button style={
{
background: theme.background, color: theme.foreground }}>
hello world
</button>
}
function Toolbar() {
return <div>
<ThemeButton></ThemeButton>
</div>
}
function App() {
return <ThemeContext.Provider value={
themes.dark}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
}
export default App
此时浏览器的显示效果如下:
现在属于黑色背景的主题。如果我们要切换为白色背景的主题,那么只需要把最底下的代码 value={themes.dark}
改为 value={themes.light}
即可。
(3)useReducer
在 redux
中,我们会用到 reducer
,但 useReducer
跟 redux
还有一点区别。 useReducer
是对单个组件进行状态
先来看一段代码:
import React, {
useReducer } from 'react'
const initialState = {
count: 0 }
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {
count: state.count + 1 }
case 'decrement':
return {
count: state.count - 1 }
default:
return state
}
}
function App() {
// 很像 const [count, setCount] = useState(0)
const [state, dispatch] = useReducer(reducer, initialState)
return <div>
count: {
state.count}
<button onClick={
() => dispatch({
type: 'increment' })}>increment</button>
<button onClick={
() => dispatch({
type: 'decrement' })}>decrement</button>
</div>
}
export default App
此时浏览器的显示效果如下:
我们将要改变的值放到 reducer
当中,之后再将 reducer
传递给 useRouter
。最后,通过 dispatch
来对值进行绑定。
下面我们来梳理一下 useRuducer
和 redux
的区别:
useReducer
是useState
的代替方案,用于处理state
的复杂变化;useReducer
是单个组件状态管理,组件通讯还需要props
;redux
是全局的状态管理,多组件共享数据。
(4)useMemo
useMemo
是 hooks
中性能优化的一部分。当我们不使用 useMemo
时,状态时这样的。看以下这段代码:
import React, {
useState } from 'react'
// 子组件
function Child({
userInfo }) {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {
userInfo.name} {
userInfo.age}</p>
</div>
}
// 父组件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
const userInfo = {
name, age: 20 }
return <div>
<p>
count is {
count}
<button onClick={
() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={
userInfo}></Child>
</div>
}
export default App
此时,控制台的打印效果如下:
大家可以看到,每每我们点击一次,不管 state
的值是否更新时,子组件 Child
都会重新打印一次。 这对于程序来说,是比较耗费性能的。因此,我们要用 useMemo
来阻止 Child
事件频繁打印,提高程序的性能。代码改造如下:
import React, {
useState, memo, useMemo } from 'react'
// 子组件
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({
userInfo }) => {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {
userInfo.name} {
userInfo.age}</p>
</div>
})
// 父组件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 用 useMemo 缓存数据,依赖于 [name]
// 当name有更新时,{ name, age: 18 } 这个缓存就会失效
const userInfo = useMemo(() => {
return {
name, age: 18 }
}, [name])
return <div>
<p>
count is {
count}
<button onClick={
() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={
userInfo}></Child>
</div>
}
export default App
此时,浏览器的显示效果如下:
大家可以看到, Child
组件除了第一次更新有打印之外,之后的没有更新就不会再进行打印。那它是怎么使用的呢,首先,我们要用 memo
把子组件给包裹下来,之后呢,通过 useMemo
去缓存数据,同时这个数据要依赖于 [name]
;值得注意的是,如果不依赖于 [name]
,它是不会生效的。
现在,我们来对 useMemo
做个总结,具体如下:
React
默认会更新所有子组件;class
组件使用shouldComponentUpdate
和PureComponent
做优化;Hooks
中使用useMemo
做优化,但优化原理和class
组件是相同的。
(5)useCallback
上面我们说到的是使用 useMemo
来缓存数据,现在我们来讲下 useCallback
。 useCallback
主要是用来缓存函数。
假设我们现在不使用 useCallback
,来运行下面这段代码:
import React, {
useState, memo, useMemo, useCallback } from 'react'
// 子组件,memo 相当于 PureComponent
const Child = memo(({
userInfo, onChange }) => {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {
userInfo.name} {
userInfo.age}</p>
<input onChange={
onChange}></input>
</div>
})
// 父组件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 用 useMemo 缓存数据
const userInfo = useMemo(() => {
return {
name, age: 21 }
}, [name])
function onChange(e) {
console.log(e.target.value)
}
return <div>
<p>
count is {
count}
<button onClick={
() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={
userInfo} onChange={
onChange}></Child>
</div>
}
export default App
此时浏览器的打印效果如下:
大家可以看到,如果没有使用 useCallback
,那么其会一直将子组件打印出来,也就是 onChange
事件会一直运转。现在,我们将 onChange
事件改造一下,具体如下:
// 用 useCallback 缓存函数
const onChange = useCallback(e => {
console.log(e.target.value)
}, [])
此时我们来看下浏览器运行的效果:
大家可以看到,加上 useCallback
之后,且 Child
没有更新时,也就不会再更新啦!
接下来我们对 useMemo
和 useCallback
做个小结:
useMemo
缓存数据;useCallback
缓存函数;- 两者都是
React Hooks
的常见优化策略。
4、自定义Hook🗞️
(1)为什么要使用自定义Hook
- 用来封装通用的功能;
- 可以开发和使用第三方
Hooks
; - 自定义
Hook
带来了无限的扩展性,解耦代码。
(2)举例阐述
假设我们现在想要封装一个 useAxios
,那怎么实现呢?
首先我们需要先在项目中安装 axios
,具体代码如下:
npm i axios --save
接着,我们在项目的 src|customHooks
文件夹下新建一个文件,命名为 useAxios.js
。具体代码如下:
import {
useState, useEffect } from 'react'
import axios from 'axios'
// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
// loading 模拟当前是否再等待中
const [loading, setLoading] = useState(false)
// 请求成功时返回的数据
const [data, setData] = useState()
// 请求失败时返回的一些信息
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 发送网络请求
setLoading(true)
axios.get(url) // 发送一个 get 请求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return [loading, data, error]
}
export default useAxios
继续,我们在项目的 src|components
文件夹,新建一个文件,命名为 CustomHookUsage.js
。这个组件主要用来使用上面所自定义的 useAxios
这个 hook
。具体代码如下:
import React from 'react'
import useAxios from '../customHooks/useAxios'
function App() {
const url = 'http://localhost:3000/'
// 数组解构
const [loading, data, error] = useAxios(url)
if (loading) return <div>loading...</div>
return error
? <div>{
JSON.stringify(error)}</div>
: <div>{
JSON.stringify(data)}</div>
}
export default App
最后,我们在项目的 App.js
中来使用它。具体代码如下:
import React, {
useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage'
function App() {
return (
<div>
{
<CustomHookUsage/>}
</div>
);
}
export default App;
此时,我们来看下浏览器的显示效果:
大家可以看到,首先是在等待的时候,它会先出现 loading
。之后呢,如果在等待结束并请求成功后,会返回 data
。当然,我们这里没有演示 error
的情况。大家可以把 url
修改为不存在的请求地址,即可测试 error
的情况。
(3)总结
看完上面的演示代码以后,我们来做个小结:
- 自定义
hook
本质是一个函数,以use
开头; - 内部可以正常使用
useState
、useEffect
或者其他Hooks
; - 自定义返回结果,格式不限;
这里给大家推荐两个第三方自定义 Hook
的库:
5、Hooks的两条重要规则🗞️
(1)Hooks使用规范
Hooks
只能用于react
函数组件和自定义Hooks
中,其他地方都不可以。比如:class组件 和 普通函数;只能用于顶层代码,不能在循环、判断中使用
Hooks
;eslint
中的插件 eslint-plugin-react-hooks ,可以帮到你使用react-hooks
。如下配置所示:// ESLint 配置文件 { "plugins": [ // ...此处省略 N 行... "react-hooks" ], "rules": { // ...此处省略 N 行 ... "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则 "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖 } }
(2)Hooks 调用顺序必须保持一致
在 react-hooks
中,调用顺序是尤为需要注意的一个关键点。为什么呢?
下面我们用一段代码来演示:
import React, {
useState, useEffect } from 'react'
function Teach({
couseName }) {
// 函数组件,纯函数,执行完即销毁
// 所以,无论组件初始化(render)还是组件更新(re-render)
// 都会重新执行一次这个函数,获取最新的组件
// 这一点和 class 组件不一样
// render: 初始化 state 的值 '张三'
// re-render: 读取 state 的值 '张三'
const [studentName, setStudentName] = useState('张三')
// if (couseName) {
// const [studentName, setStudentName] = useState('张三')
// }
// render: 初始化 state 的值 '双越'
// re-render: 读取 state 的值 '双越'
const [teacherName, setTeacherName] = useState('星期一研究室')
// if (couseName) {
// useEffect(() => {
// // 模拟学生签到
// localStorage.setItem('name', studentName)
// })
// }
// render: 添加 effect 函数
// re-render: 替换 effect 函数(内部的函数也会重新定义)
useEffect(() => {
// 模拟学生签到
localStorage.setItem('name', studentName)
})
// render: 添加 effect 函数
// re-render: 替换 effect 函数(内部的函数也会重新定义)
useEffect(() => {
// 模拟开始上课
console.log(`${
teacherName} 开始上课,学生 ${
studentName}`)
})
return <div>
课程:{
couseName},
讲师:{
teacherName},
学生:{
studentName}
</div>
}
export default Teach
上面的代码演示的是一个函数组件。那对于函数组件来说,它是一个纯函数,一般执行完就会销毁。因此,无论是组件初始化(render)还是组件更新(re-render),都会重新执行一次这个函数,获取最新的组件。这一点和 class
组件不一样。
因此,假设我们把上面的 const [studentName, setStudentName] = useState('张三')
给用 if
语句包裹起来,那么它在执行时会受到阻碍。同时,沿着底下的顺序走下去,有可能就会把 张三
这个值,赋值给 teacherName
。因此,在 react-hooks
中,我们不能将 react-hooks
用在循环和判断里面,不然会很容易导致调用顺序错乱。
依据以上的解析,我们来对这个规则做个小结:
- 无论是
render
还是re-render
,Hooks
调用顺序必须是一致的; - 如果
Hooks
出现在循环、判断里,则无法保证顺序一致; Hooks
严重依赖于调用顺序!重要!
三、⌨️React-Hooks组件逻辑复用
1、class组件的逻辑复用
class
组件有三种逻辑复用形式。分别是:
- Mixin
- 高阶组件
HOC
- Render Prop
下面说下它们三者各自的缺点。
(1)Mixin
- 变量作用域来源不清
- 属性重名
mixins
引入过多会导致顺序冲突
(2)高阶组件 HOC
- 组件层级嵌套过多,不易渲染,不易调试
HOC
会劫持props
,必须严格规范,容易出现疏漏
(3)Render Prop
- 学习成本高,不易理解
- 只能传递纯函数,而默认情况下纯函数功能有限
了解了三种 class
组件的缺点之后,现在,我们来看下如何使用 Hooks
做组件逻辑复用。
2、使用 Hooks 做组件逻辑复用
使用 hooks
来使得组件可以进行逻辑复用的本质是:自定义 hooks
。下面我们用一个例子来展示。
(1)例子阐述
首先,我们在项目的 src|customHooks
文件夹下新建一个文件,命名为 useMousePosition.js
。具体代码如下:
import {
useState, useEffect } from 'react'
function useMousePosition() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(() => {
function mouseMoveHandler(event) {
// event.clientX 可以拿到鼠标横坐标和纵坐标的位置
setX(event.clientX)
setY(event.clientY)
}
// 绑定事件
document.body.addEventListener('mousemove', mouseMoveHandler)
// 解绑事件
return () => document.body.removeEventListener('mousemove', mouseMoveHandler)
}, [])
return [x, y]
}
export default useMousePosition
继续,我们在项目的 src|components
文件夹,新建一个文件,命名为 CustomHookUsage.js
。这个组件主要用来使用上面 useMousePosition
中的逻辑。具体代码如下:
import React from 'react'
import useMousePosition from '../customHooks/useMousePosition'
function App() {
const [x, y] = useMousePosition()
return <div style={
{
height: '500px', backgroundColor: '#ccc' }}>
<p>鼠标位置 {
x} {
y}</p>
</div>
}
export default App
最后,我们在项目的 App.js
中来使用它。具体代码如下:
import React, {
useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage'
function App() {
return (
<div>
{
<CustomHookUsage/>}
</div>
);
}
export default App;
此时,我们来看下浏览器的显示效果:
使用 hooks
做组件逻辑复用,要比 class
组件要简便和灵活许多。下面我们来梳理下 hooks
做组件逻辑复用的好处。
(2)好处
Hooks
做组件逻辑复用的好处:
- 完全符合
Hooks
原有的规则,没有其他要求,易理解和记忆; - 变量作用域很明确;
- 不会产生组件嵌套。
四、📀React Hooks 注意事项
1、useState初始化值,只有第一次有效
我们先来看一段代码,具体如下:
import React, {
useState } from 'react'
// 子组件
function Child({
userInfo }) {
// render: 初始化 state
// re-render: 只恢复初始化的 state 值,不会再重新设置新的值
// 只能用 setName 修改
const [ name, setName ] = useState(userInfo.name)
return <div>
<p>Child, props name: {
userInfo.name}</p>
<p>Child, state name: {
name}</p>
</div>
}
function App() {
const [name, setName] = useState('星期一')
const userInfo = {
name }
return <div>
<div>
Parent
<button onClick={
() => setName('星期二')}>setName</button>
</div>
<Child userInfo={
userInfo}/>
</div>
}
export default App
此时,浏览器的显示效果如下:
大家可以看到,当我们点击的时候,只改变了 useInfo.name
的值,也就是第一个 Child
的值。而此时,可能有的小伙伴会产生疑惑说, useInfo.name
的值是赋值给 name
的,那为什么第二个 Child
不会改变呢。
原因在于,对于 useState
来说,刚开始我们总是会给 state
设置一个初始化的值,而这个初始化的值,即使是数据更新了,它只有第一次的值是有效的,更新的值不会被赋值到其中。所以下面第二个 Child
中 name
值就一直是星期一。
2、useEffect内部不能修改state
先来模拟一段代码:
import React, {
useState, useEffect } from 'react'
function UseEffectChangeState() {
const [count, setCount] = useState(0)
// 模拟 DidMount
useEffect(() => {
console.log('useEffect...', count)
// 定时任务
const timer = setInterval(() => {
console.log('setInterval...', count)
setCount(count + 1)
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, []) // 依赖为 []
// 依赖为 [] 时: re-render 不会重新执行 effect 函数
// 没有依赖时:re-render 会重新执行 effect 函数
return <div>count: {
count}</div>
}
export default UseEffectChangeState
此时我们来看下浏览器的效果:
大家可以看到,如果按照我们预期所想的,那么应该会递增地打印出 1234
,但是最终没有这么显示,timer
里面打印的一直是初始值 0
,也就是说,这个 state
在 useEffect
里面并不起作用。
因此,这一点在我们日常的开发中需要尤为注意,有可能代码写着写着 bug
就出现在这了。
那既然问题出现了,我们就需要来想一下它的解决方案了。改造如下:
import React, {
useState, useRef, useEffect } from 'react'
function UseEffectChangeState() {
const [count, setCount] = useState(0)
// 模拟 DidMount
const countRef = useRef(0)
useEffect(() => {
console.log('useEffect...', count)
// 定时任务
const timer = setInterval(() => {
console.log('setInterval...', countRef.current)
// setCount(count + 1)
setCount(++countRef.current)
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, []) // 依赖为 []
// 依赖为 [] 时: re-render 不会重新执行 effect 函数
// 没有依赖:re-render 会重新执行 effect 函数
return <div>count: {
count}</div>
}
export default UseEffectChangeState
此时浏览器的显示效果如下:
看上面的代码我们可以发现,通过把 countRef
放到 useEffect
外部,并使用 useRef
,以此来对 state
的值进行修改,达到我们最终的目的。
3、useEffect可能出现死循环
假设我们要利用 axios
发送网络请求,并且有想要给 url
加一些配置,那么我们有可能这么处理:
import {
useState, useEffect } from 'react'
import axios from 'axios'
// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url, config = {
}) {
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 发送网络请求
setLoading(true)
axios(url, config) // 发送一个 get 请求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url, config])
return [loading, data, error]
}
export default useAxios
看着是挺合理,但是呢,当config
使用的是引用数据类型时,也就是 config={}
或者 confgi=[]
时, useEffect
会出现死循环,无限的发送请求。因此,当我们想要给 axios
传值时,只能传基本数据类型的。改造代码如下:
import {
useState, useEffect } from 'react'
import axios from 'axios'
// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
const [loading, setLoading] = useState(false
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 发送网络请求
setLoading(true)
axios.get(url) // 发送一个 get 请求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return [loading, data, error]
}
export default useAxios
这样,才不会出现死循环地一直翻滚的对接口发送数据请求。
五、🔍结束语
在上面的文章中,我们首先认识了 hooks
的基本概念,接着,我们重点阐述了 state hook
和 effect hook
,同时,我们也简单地了解了其他几种 hooks
。最后,谈到了 react-hooks 在组件逻辑复用方面的贡献,以及 react-hooks
需要注意一些事项。
到这里,关于 react-hooks
的介绍就结束啦!不知道大家对 react-hooks
又有了一些新的了解呢?
🐣彩蛋 One More Thing
(:往期推荐
react专栏戳这里 https://juejin.cn/column/7018019097656950815
(:叮
- 关注公众号星期一研究室,第一时间关注优质文章,更有「offer来了」面试专栏待你解锁~
- 如果您觉得这篇文章有帮助到您的的话记得留个脚印jio再走哟~~😉
- 以上就是本文的全部内容!我们下期见!👋👋👋