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
useRef返回值看作一个组件内部全局共享变量,它会在渲染内部共享一个相同的值。相对state/props他们是独立于不同次render中的内部作用域值。
注意
同时额外需要注意 useRef 返回值的改变并不会引起组件重新 render,这也是和 state/props 不同的地方。
- 当然我们在 React.functionComponent 中想要获取对应 jsx 的真实 Dom 元素时候也可以通过 useRef 进行获取到对应的 Dom 元素。
自定义 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
警告
redux 内容 暂时废弃
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))

