React Hook
Class 组件存在的问题
复杂组件变得难以理解:
最初编写一个
class
组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class
组件会变得越来越复杂比如
componentDidMount
中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount
中移除)而对于这样的
class
实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度难以理解的
class
:学习 ES6 的
class
是学习React
的一个障碍在
class
中,我们必须搞清楚this
的指向到底是谁实现组件状态逻辑复用很难:
在前面为了组件状态逻辑复用我们需要通过高阶组件或
render props
(🔎 详情见 react 组件化)像我们之前学习的
redux
中connect
或者react-router
中的withRouter
,这些高阶组件设计的目的就是为了状态的复用或者类似于
Provider
、Consumer
来共享一些状态,但是多次使用Consumer
时,我们的代码就会存在很多嵌套这些代码让我们不管是编写和设计上来说,都变得非常困难
为什么使用 Hook
Hook
是 React 16.8
的新增特性,它可以让我们在不编写 class
组件的情况下使用 state
以及其他的 React
特性(比如生命周期)
我们先来思考一下 class
组件相对于函数式组件有什么优势:
class
组件可以定义自己的state
,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量class
组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;比如在componentDidMount
中发送网络请求,并且该生命周期函数只会执行一次;函数式组件在学习hooks
之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;class
组件可以在状态改变时只会重新执行render
函数以及我们希望重新调用的生命周期函数componentDidUpdate
等,函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次
所以,在 Hook
出现之前,对于上面这些情况我们通常都会编写 class
组件
Hook 规则
只能在 React
函数组件顶层和自定义钩子中使用
useState
useState
会帮助我们定义一个 state
变量,它与 class
里面的 this.state
提供的功能完全相同。一般来说,在函数退出后变量就会“消失”,而 state
中的变量会被 React
保留
- 参数:任何类型的初始化值,如果是函数,期望是一个无参的纯函数,函数的返回值就是初始化值
- 返回值:数组,包含两个元素
- 元素一:当前状态的值
- 元素二:设置状态值的函数
import { useState } from 'React'
export default function App() {
const [friends, setFriends] = useState([
{
name: 'frank',
age: 10,
},
{
name: 'zhang',
age: 123,
},
])
function addAge(index) {
const newFriends = [...friends]
newFriends[index].age += 1
setFriends(newFriends)
}
return (
<div>
<ul>
{friends.map((item, index) => {
return (
<li key={index}>
{item.name},{item.age},
<button
onClick={(e) => {
addAge(index)
}}
>
age+1
</button>
</li>
)
})}
</ul>
</div>
)
}
useEffect
副作用
副作用是函数式编程里的概念,要彻底理解副作用,首先解释纯函数(Pure function):返回结果只依赖于它的参数,而且没有任何可观察的副作用。函数与外界交流数据只有一个唯一渠道——参数和返回值。
第一点:给纯函数传入相同的参数,永远会返回相同的值。如果返回值依赖外部变量,则不是纯函数。
// 纯函数 不管外部如何天翻地覆,只要传入的参数是确定的,那么值永远是可预料的。
const foo = (a, b) => a + b
foo(1, 2) // => 3
// 非纯函数 返回值也依赖外部变量a,结果无法预料
const a = 1
const foo = (b) => a + b
foo(2)
第二点:一个函数在执行过程中产生了外部可观察的变化,则这个函数是有副作用(Side Effect)的。通俗点就是函数内部做了和运算返回值无关的事,比如修改外部作用域/全局变量、修改传入的参数、发送请求、console.log、手动修改 DOM 都属于副作用。
const foo = (obj, b) => {
obj.x = 2 // 修改了外部变量
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2
React
组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用 filter
、map
等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中
例如,如果直接将修改 state
的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出:
react-dom.development.js:16317 Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
为了解决这个问题 React
专门为我们提供了钩子函数 useEffect
,Effect
的翻译过来就是副作用,专门用来处理那些不能直接写在组件内部的代码。
哪些代码不能直接写在组件内部呢?最常见的就是获取数据、设置定时器。简单来说,就是那些和组件渲染无关,但却有可能对组件产生副作用的代码
useEffect
要求我们传入一个回调函数,默认情况下,无论是第一次渲染之后,还是每次 DOM
更新之后,都会执行这个回调函数
import { useEffect, useState } from 'react'
export default function App() {
const [count, setCount] = useState(1)
useEffect(() => {
// 网页标题和count同步
// 如果采用class组件实现相同的功能
// 需要在componentDidMount,componentDidUpdate 两个生命周期函数中,
// 编写相同的逻辑代码
document.title = count
})
return (
<div>
{count}
<button
onClick={() => {
setCount((prevCount) => prevCount + 1)
}}
>
+1
</button>
</div>
)
}
清除 Effect
在 class
组件的编写过程中,某些副作用的代码,我们需要 componentWillUnmount
中进行清除,比如清除定时器
useEffect
传入的回调函数A
有一个返回值,这个返回值是另外一个回调函数B
B 的执行时机:
- 组件卸载的时候
- 在有依赖项的情况下,React 将首先使用旧值运行回调函数
B
,然后使用新值运行回调函数A
useEffect(() => {
// 回调函数A
return () => {
// 回调函数B
}
})
// 一个例子
import React, { useState } from 'react'
const ParentComponent = () => {
const [isComponentVisible, setComponentVisible] = useState(true)
const handleUnmountComponent = () => {
setComponentVisible(false)
}
return (
<div>
{isComponentVisible && <ChildComponent />}
<button onClick={handleUnmountComponent}>卸载子组件</button>
</div>
)
}
const ChildComponent = () => {
React.useEffect(() => {
console.log('Child component is mounted.')
return () => {
console.log('Child component is unmounted.')
}
}, [])
return <div>Child Component</div>
}
export default ParentComponent
多个 Effect
Hook
允许我们按照代码的用途分离它们, 而不是像生命周期函数那样,React
将按照 effect
声明的顺序依次调用组件中的每一个 effect
限制 Effect
默认情况下,useEffect
的回调函数会在每次渲染时都重新执行,但是这会导致两个问题
某些代码我们只是希望执行一次即可,多次执行也会导致一定的性能问题
useEffect
有两个参数:
- 参数一:执行的回调函数
- 参数二:一个数组;其中存放的元素发生变化时,
effect
会重新执行;如果数组中有多个元素,即使只有一个元素发生变化,React
也会执行effect
如果想执行只运行一次的 effect
,可以传递一个空数组([]
)作为第二个参数。这就告诉 React
你的 effect
不依赖于 props
或 state
中的任何值,所以它永远都不需要重复执行(只在组件初始渲染时执行一次)
useLayoutEffect
useEffect
发生在视图更新后,而 useLayoutEffect
发生在编译成 dom
元素之后视图更新之前
useEffect
的代码是按照顺序执行的,但 useLayoutEffect
总是比 useEffect
先执行
由于 useLayoutEffect
的代码是跟DOM
操作相关的,所以最好在里面写跟 DOM
相关的代码
如果业务需求是先要进行 DOM
操作或者跟页面布局相关的,那么就可以使用 useLayoutEffect
useContext
在之前的开发中,我们要在组件中使用共享的 Context
有两种方式:
- 类组件可以通过 类名.contextType = MyContext 方式,在类中获取
context
- 多个
Context
或者在函数式组件中通过MyContext.Consumer
方式共享context
但是多个 Context
共享时的方式会存在大量的嵌套
Context Hook
允许我们通过 Hook
来直接获取某个Context
的值:
import React, { Component, useContext, useEffect } from 'react'
const MyContext = React.createContext()
const MyContext2 = React.createContext()
function User() {
const user = useContext(MyContext)
const user2 = useContext(MyContext2)
useEffect(() => {
console.log(user, user2)
})
return (
<div>
{user.age},{user.name}
</div>
)
}
export default class App extends Component {
constructor() {
super()
this.state = {
name: 'frank',
age: 123,
}
}
render() {
return (
<div>
<MyContext.Provider value={this.state}>
<MyContext2.Provider value={{ name: 'frank123' }}>
<User />
</MyContext2.Provider>
</MyContext.Provider>
</div>
)
}
}
useMemo、useCallback
(useMemo 与 useCallback 最佳实践)[/Log/React/useMemo 与 useCallback 最佳实践]
基础用法
useMemo 常用来缓存引用类型值(JSX 元素类似于 JavaScript 中的对象,也被视为引用类型值),useCallBack 常用来缓存函数
第二个参数的依赖数组:
空数组,重渲染时永远返回同一引用类型值
变量数组,当变量发生变化时,才返回新的引用类型值
总结
useMemo
一方面可以将其看成就是 React.memo
的替代品,同时可以对任意大小的 JSX
片段进行缓存
另一方面其实它让我们能够对组件内部的各个元素进行更细粒度的控制,让我们能够不只是利用 React.memo
粗暴的对整个组件进行记忆,而可以针对特定片段进行缓存与复用
useCallBack
并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变,useMemo
亦然
与 React.memo
或是 useMemo
联用时,才会因为传递给子组件相同的函数(地址相同),从而避免不必要的子组件渲染(只有在这种场景下才是有意义的:当组件的每一个 prop,以及子组件本身被缓存的时候):
const useInputWithCallBack = () => {
const [value, setValue] = useState('')
const handleChange = useCallback((e) => {
console.log('handeleChange,重新生成')
setValue(e.target.value)
}, [])
return { value, handleChange }
}
const Inner = memo((props) => {
return <input type="text" onChange={props.onChange} />
})
const Test = () => {
const { value, handleChange } = useInputWithCallBack()
return (
<div>
<h3>name: {value}</h3>
<Inner onChange={handleChange} />
</div>
)
}
const Inner = (props) => {
return <input type="text" onChange={props.onChange} />
}
const Test = () => {
const { value, handleChange } = useInputWithCallBack()
return (
<div>
<h3>name: {value}</h3>
{useMemo(
() => (
<Inner onChange={handleChange} />
),
[handleChange]
)}
</div>
)
}
useRef
// TODO
useRef
返回一个 ref
对象 , current
属性被初始化为传入的参数
在组件的整个生命周期中,永远返回相同的对象
和 state
变量的区别是,更改 current
不会引起组件的重新渲染
useRef
通常用来获取 DOM
元素:
import { useRef } from 'react'
export default () => {
const titleRef = useRef()
const inputRef = useRef()
const changeDOM = () => {
inputRef.current.focus()
titleRef.current.innerHTML = 'hello,sb'
}
return (
<div>
<input type="text" ref={inputRef} />
<h2 ref={titleRef}>hello,html!</h2>
<button onClick={changeDOM}>changeDOM</button>
</div>
)
}
自定义 Hook
自定义 Hook
本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算 React
的特性
案例 1:所有的组件在创建和销毁时都进行打印
组件被创建:打印 组件被创建了;组件被销毁:打印 组件被销毁了
import { useEffect, useState } from 'react'
const Com1 = () => {
usePrintLog('com1')
return <h2>Com1</h2>
}
const Com2 = () => {
usePrintLog('com2')
return <h2>Com2</h2>
}
const Com3 = () => {
usePrintLog('com3')
return <h2>Com3</h2>
}
export default () => {
const [display, setdisplay] = useState(true)
return (
<div>
{display ? <Com1 /> : <h2>Com1销毁</h2>}
{display ? <Com2 /> : <h2>Com2销毁</h2>}
{display ? <Com3 /> : <h2>Com3销毁</h2>}
<button
onClick={() => {
setdisplay(!display)
}}
>
display?
</button>
</div>
)
}
const usePrintLog = (name) => {
useEffect(() => {
console.log(`${name}创建了`)
return () => {
console.log(`${name}销毁了`)
}
}, [])
}
案例 2:Context 的共享
import { userContext } from '../11_useHook_共享context/app'
import { useContext } from 'react'
// 自定义Hook
export default function useUserContext() {
const user = useContext(userContext)
return [user]
}
// 在组件中使用
const User = () => {
const [user] = useUserContext()
return (
<div>
<h2>{user.name}</h2>
</div>
)
}
案例 2:获取滚动位置
import { useEffect, useState } from 'react'
function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0)
const handleScroll = () => {
setScrollPosition(window.scrollY)
}
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return scrollPosition
}
export default useScrollPosition
import useScrollPosition from './Hook/useScrollPosition'
export default () => {
const scrollPositon = useScrollPosition()
return (
<div style={{ height: '2000px' }}>
<h2 style={{ position: 'fixed' }}>当前滚动位置:{scrollPositon}</h2>
</div>
)
}
案例 3:localStorage 存储
import { useEffect, useState } from 'react'
export default (key, initialValue) => {
const [value, setValue] = useState(() => {
const storageValue = window.localStorage.getItem(key)
return storageValue ? JSON.parse(storageValue) : initialValue
})
useEffect(() => {
console.log('useEffect')
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}
useImperativeHandle
// TODO
redux hook
useSelector
在之前的redux
开发中,为了让组件和redux
结合起来,需要使用react-redux
中的connect
函数,将组件和redux
进行连接,然后通过mapStateToProps
函数将redux
中的state
映射到组件的props
现在使用 useSelector
、useDispatch
等 hook
替代 connect
,大大降低了心智负担
useSelector
const count = useSelector((state) => state.countStore.count)
useDispatch
先将需要派发的 action
导入到组件中,然后使用 useDispatch
获取 dispatch
import { useDispatch } from 'react-redux'
// ....
const dispatch = useDispatch()
// ....
dispatch(addNumber(3))