为什么会有 hooks?
-
React Hooks 是 React 16.8 版本新增的特性
-
让函数组件具有类组件的状态和生命周期管理能力
useState
-
setState支持stateless组件有自己的state
-
入参:具体值或一个函数
-
返回值:数组
-
第一项是state值
-
第二项负责派发数据更新,组件渲染
-
setState会让组件重新执行 => 配合useMemo或useCallback
const DemoState = (props) => {
/* number为此时state读取值 ,setNumber为派发更新的函数 */
const [number, setNumber] = useState(0) /* 0为初始值 */
return (
<div>
<span>{ number }</span>
<button onClick={ ()=> {
setNumber(number + 1)
console.log(number) /* 这里的number是不能够即使改变的,返回0 */
}}
/>
</div>
)
}
// 当更新函数之后,state的值是不能即时改变的,只有当下一次上下文执行的时候,state值才随之改变
const a =1
const DemoState = (props) => {
/* useState 第一个参数如果是函数 则处理复杂的逻辑,返回值为初始值 */
let [number, setNumber] = useState(()=>{
// number
return a === 1 ? 1 : 2
}) /* 1为初始值 */
return (<div>
<span>{ number }</span>
<button onClick={ ()=>setNumber(number+1) } ></button>
</div>)
}
useEffect
-
使用条件
-
当组件init、dom render完成
-
操纵dom
-
请求数据(如componentDidMount)
-
-
不限制条件,组件每次更新都会触发useEffect => componentDidUpdate 与 componentwillreceiveprops
-
第一个参数为处理事件,第二个参数接收数组,为限定条件
- 当数组变化时触发事件,为[]只在组件初始化时触发
-
第一个参数有返回时 => 消除副作用
- 去除定时器、事件绑定
/* 模拟数据交互 */
function getUserInfo(a){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name:a,
age:16,
})
},500)
})
}
const Demo = ({ a }) => {
const [ userMessage , setUserMessage ] = useState({})
const [number, setNumber] = useState(0)
const div= useRef()
const handleResize =()=>{}
useEffect(()=>{
getUserInfo(a).then(res=>{
setUserMessage(res)
})
console.log(div.current) /* div */
window.addEventListener('resize', handleResize)
/*
只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,
如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount
*/
},[ a ,number ])
return (<div ref={div} >
<span>{ userMessage.name }</span>
<span>{ userMessage.age }</span>
<div onClick={ ()=> setNumber(1) } >{ number }</div>
</div>)
}
const Demo = ({ a }) => {
const handleResize = () => {}
useEffect(() => {
const timer = setInterval(() => console.log(666), 1000)
window.addEventListener('resize', handleResize)
/* 此函数用于清除副作用 */
return function () {
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
}, [a])
return <div></div>
}
useEffect无法直接使用async await
useEffect(() => {
const fetchData = async () => {
const data = await fetch('https://xxx.com')
const json = await response.json()
setData(json)
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error)
}, [])
监听多个属性变化
import React, { useEffect } from 'react'
function MyComponent({ prop1, prop2 }) {
useEffect(() => {
// 当 prop1 或 prop2 发生变化时执行的代码
console.log('prop1 或 prop2 发生了变化')
// 在这里可以执行你想要的操作,比如发送网络请求、更新状态等等
if (prop1 && prop2) {
// 当 prop1 和 prop2 都满足条件时执行的代码
console.log('prop1 和 prop2 都发生了变化')
// 在这里可以执行你想要的操作,比如发送网络请求、更新状态等等
}
}, [prop1, prop2])
return (
// 组件的 JSX
111
)
}
export default MyComponent
useLayoutEffect
渲染更新之前的 useEffect
-
useEffect
- 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调
-
useLayoutEffect
- 组件更新挂载完成 -> 执行useLayoutEffect回调-> 浏览器dom 绘制完成
-
渲染组件
-
useEffect:闪动
-
useLayoutEffect:卡顿
-
// 用 useLayoutEffect 不能看出来 dom 的变化
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我们需要在dom绘制之前,移动dom到制定位置*/
const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
useContext
-
用来获取父级组件传递过来的context值
- 这个当前值就是最近的父级组件 Provider 的value
-
从parent comp获取ctx方式
-
useContext(Context)
-
Context.Consumer
-
/* 用useContext方式 */
const DemoContext = () => {
const value = useContext(Context)
/* my name is aaa */
return <div> my name is {value.name}</div>
}
/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
return <Context.Consumer>
{/* my name is aaa */}
{ (value)=> <div> my name is { value.name }</div> }
</Context.Consumer>
}
export default () => {
return (
<div>
<Context.Provider value={{ name: 'aaa' }}>
<DemoContext />
<DemoContext1 />
</Context.Provider>
</div>
)
}
useReducer
-
入参
-
第一个为函数,可以视为reducer
- 包括state 和 action
-
返回值 state (base on action)
-
第二个为state的初始值
-
-
出参
-
第一个是更新后的state值
-
第二个是派发更新的dispatch函数
-
执行dispatch会导致组件re-render
- (另一个是useState)
-
-
-
useReducer+useContext 代替Redux
const DemoUseReducer = () => {
/* number为更新后的state值, dispatchNumber 为当前的派发函数 */
const [number, dispatchNumber] = useReducer((state, action) => {
const { payload, name } = action
/* return的值为新的state */
switch (name) {
case 'a':
return state + 1
case 'b':
return state - 1
case 'c':
return payload
}
return state
}, 0)
return (
<div>
当前值:{number}
{/* 派发更新 */}
<button onClick={() => dispatchNumber({ name: 'a' })}>增加</button>
<button onClick={() => dispatchNumber({ name: 'b' })}>减少</button>
<button onClick={() => dispatchNumber({ name: 'c', payload: 666 })}>赋值</button>
{/* 把dispatch 和 state 传递给子组件 */}
<MyChildren dispatch={dispatchNumber} State={{ number }} />
</div>
)
}
useMemo
- 根据useMemo的第二个参数deps(数组)判定是否满足当前的限定条件来决定是否执行第一个cb
// selectList 不更新时,不会重新渲染,减少不必要的循环渲染
useMemo(() => (
<div>{
selectList.map((i, v) => (
<span
className={style.listSpan}
key={v} >
{i.patentName}
</span>
))}
</div>
), [selectList])
// listshow, cacheSelectList 不更新时,不会重新渲染子组件
useMemo(() => (
<Modal
width={'70%'}
visible={listshow}
footer={[
<Button key="back">取消</Button>,
<Button key="submit" type="primary">
确定
</Button>,
]}
>
{/* 减少了PatentTable组件的渲染 */}
<PatentTable
getList={getList}
selectList={selectList}
cacheSelectList={cacheSelectList}
setCacheSelectList={setCacheSelectList}
/>
</Modal>
),
[listshow, cacheSelectList]
)
// 减少组件更新导致函数重新声明
const DemoUseMemo = () => {
/* 用useMemo 包裹之后的log函数可以避免了每次组件更新再重新声明 ,可以限制上下文的执行 */
const newLog = useMemo(() => {
const log = () => {
console.log(123)
}
return log
}, [])
return <div onClick={() => newLog()}></div>
}
// 如果没有加相关的更新条件,是获取不到更新之后的state的值的
const DemoUseMemo = () => {
const [number, setNumber] = useState(0)
const newLog = useMemo(() => {
const log = () => {
/* 点击span之后 打印出来的number 不是实时更新的number值 */
console.log(number)
}
return log
/* [] 没有 number */
}, [])
return (
<div>
<div onClick={() => newLog()}>打印</div>
<span onClick={() => setNumber(number + 1)}>增加</span>
</div>
)
}
useCallback
useMemo返回cb的运行结果
useCallback返回cb的函数
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
const MemoizedButton0 = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
const handleClick = () => {
console.log('handleClick');
increaseCount(clickCount + 1);
}
return (
<div>
<p>{clickCount}</p>
<MemoizedButton0 handleClick={handleClick}>Click</MemoizedButton0>
</div>
)
}
// MemoizedButton0还是重新渲染了
// Index组件state发生变化,导致组件重新渲染
// 每次渲染导致重新创建内部函数handleClick
// 进而导致子组件Button也重新渲染
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
const MemoizedButton1 = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
// 这里使用了`useCallback`
const handleClick = useCallback(() => {
console.log('handleClick');
increaseCount(clickCount + 1);
}, [])
return (
<div>
<p>{clickCount}</p>
<MemoizedButton1 handleClick={handleClick}>Click</MemoizedButton1>
</div>
)
}
实战
是否所有依赖都要放在依赖数组中
- 该变量变化时,需要触发 useEffect 函数执行 => 把变量放到 deps 数组中
尽量不要用useCallback
-
useCallback 大部分场景没有提升性能
-
useCallback让代码可读性变差
useMemo建议适当使用
- 在deps不变,且非简单的基础类型运算的情况下建议使用
useState的正确使用姿势
-
能用其他状态计算出来就不用单独声明状态
- 一个 state 必须不能通过其它 state/props 直接计算出来,否则就不用定义 state
-
保证数据源唯一,在项目中同一个数据,保证只存储在一个地方
-
useState 适当合并
自定义Hooks - 本质:实现一个函数
setTitle hook
import { useEffect } from 'react'
const useTitle = (title) => {
useEffect(() => {
document.title = title
}, [])
return
}
export default useTitle
const App = () => {
useTitle('new title')
return <div>home</div>
}
update hook
import { useState } from 'react'
const useUpdate = () => {
const [, setFlag] = useState()
const update = () => {
setFlag(Date.now())
}
return update
}
export default useUpdate
// 实际使用
const App = (props) => {
// ...
const update = useUpdate()
return (
<div>
{Date.now()}
<div>
<button onClick={update}>update</button>
</div>
</div>
)
}
useScroll hooks
import { useState, useEffect } from 'react'
const useScroll = (scrollRef) => {
const [pos, setPos] = useState([0, 0])
useEffect(() => {
function handleScroll(e) {
setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
}
scrollRef.current.addEventListener('scroll', handleScroll)
return () => {
scrollRef.current.removeEventListener('scroll', handleScroll)
}
}, [])
return pos
}
export default useScroll
// 用法
import React, { useRef } from 'react'
import { useScroll } from 'hooks'
const Home = (props) => {
const scrollRef = useRef(null)
const [x, y] = useScroll(scrollRef)
return (
<div>
<div ref={scrollRef}>
<div className="innerBox"></div>
</div>
<div>
{x}, {y}
</div>
</div>
)
}
Hooks VS HOC
-
Hook
-
把更相关的逻辑放在一起 => 取代掉生命周期
-
适合做 Controller 或者需要内聚的相关逻辑
-
-
高阶组件
-
将外部的属性功能放到一个基础 Component 中 => 扩展能力的插件
-
react-swipeable-views中的 autoPlay 高阶组件
-
通过注入状态化的 props 的方式对组件进行功能扩展,而不是直接将代码写在主库中)
-
-
react hooks为什么不能放在if和for里?
-
React 顺序调用 Hook
-
在 if 和 for 中的执行次数是动态的,可能会导致 Hook 的调用顺序发生改变,从而引发问题
- 在某个循环中使用 useState Hook,由于循环次数是不定的,Hook 的调用顺序也就无法确定,会导致状态更新混乱。
-
只能在函数组件的顶层作用域中使用 Hook