Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hooks的使用规则
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
✅ 在 React 的函数组件中调用 Hook
✅ 在自定义 Hook 中调用其他 Hook
官方还提供了 linter插件 用来强制hooks的使用规则。
useState useEffect useRef
useState
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
const ClickCount = () => {
const [ count, setCount ] = useState(0)
const [ num, setNam ] = useState(1)
const handleClick = () => {
setCount(count+1)
setNam(num+2)
}
return (
<div>
<p>count:{count}---num:{num}</p>
<button onClick={handleClick}>+1</button>
</div>
)
}
如果异步更新状态时,类似类组件中setState更新状态的方式,useState也提供了两种更新状态的方式:
- 直接使用setCount()
setCount(count+1)
- 使用setCount函数式更新
setCount((prevCount) => {
return prevCount + 1
})
useEffect
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。 useEffect相当于类组件中的三个生命周期函数分别是:
componentDidMount、componentDidUpdate、componentWillUnmount
useEffect传入两个参数:
- 第一个参数是一个函数用来执行某些操作。
return一个函数用来清除定时器或订阅等资源。--- 模拟componentWillUnmount生命周期 - 第二个参数是一个数组,用来设置useEffect函数执行的依赖项,控制useEffect函数是否被重新执行。--- 模拟
componentDidUpdate生命周期 组件刚加载时就会执行useEffect,相当于componentDidMount生命周期。
const Fetch = () => {
const [result, setResult] = useState(null)
useEffect(()=> {
fetch('./index.html').then(response=>response.text()).then(res=>{
// 此时会输出两次,组件刚初始化时会触发一次,
//当状态改变了会再触发一次分别相当于【componentDidMount和componentDidUpdate】
console.log(111)
setResult(res)
})
}, [])
// 如果第二个参数为空数组会使useEffect执行一次,【componentDidMount】
// [result] 会执行两次取决于result的状态
// [a,b]有一个状态发生改变都会重新执行
return (
<div>
result: {result}
</div>
)
}
const Fetch1 = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count+1)
}, 500);
// 每次依赖项发生变化后会执行
return () => {
//清除上次的定时器
clearInterval(timer)
}
},[count])
// 如果不添加依赖,会造成状态得不更新,使得react中的capture value不能检测到状态的更新,就不会重新执行useEffect【此时在这个例子中就是定时器不能重复执行】,
// 如果添加依赖,就会造成useEffect的重复的生成回调函数
// 解决:useRef
return (
<div>
count: {count}
</div>
)
}
useRef
useRef的用法主要有两个:
- 用来标记DOM
- 用来保存数据 用来标记DOM
首先通过const btnRef = useRef(null)声明一个标记名称,然后在需要标记的DOM上通过ref={btnRef}做标记,最后通过btnRef调用。
const Fetch1 = () => {
const [count1, setCount1] = useState(0)
const btnRef = useRef(null)
// const handleOnclick = () => {
// setCount1(count1+1)
// }
useEffect(() => {
console.log(222)
const handleOnclick = () => {
setCount1(count1+1)
}
btnRef.current.addEventListener('click',handleOnclick, false)
return () => btnRef.current.removeEventListener('click', handleOnclick,false)
},[count1])
return (
<div>
count1: {count1}
<hr></hr>
{/* <button onClick={handleOnclick}>+1</button> */}
<button ref={btnRef}>+1</button>
</div>
}
用来保存数据
在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state可以在多次渲染之后依旧不变。但是,state的问题在于一旦修改了它就会造成组件的重新渲染。
那么这个时候就可以使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。
import React, { useState, useEffect, useRef } from 'react'
function Counter() {
const [count, setCount] = useState(0);
const timer = useRef(null)
useEffect(() => {
console.log('usesEffect'); // 只会在加载组件的时候输出一次,使用useRef不会引起组件的重新渲染
timer.current = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => {
clearInterval(timer.current)
};
}, []);
return (
<div>
<p>count: {count}</p>
</div>
)
}
export default Counter
memo useMemo useCallback
memo
memo用于在函数式组件中优化功能,通过调用子组件时传递给子组件的数据前后两次是否相同,来决定子组件是否重新渲染。
向子组件传递简单数据类型的值
import React, {memo, useState} from 'react';
const Parent = () => {
const [count, setCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
return (
<div>
{<!-- 点击按钮不会改变传入子组件的数据 -->}
count: {count}
<button onClick={()=>{
setCount(count+1)
}}>+1</button>
{<!--点击按钮,改变传入子组件的数据 -->}
<button onClick={() => {
setClickCount(clickCount + 1)
}}>GET CURRENT TIME</button>
<Child count={clickCount}/>
</div>
)
}
const Child = memo((props) => {
// console.log(props)
console.log(123)
const date = new Date()
return (
<div>
<p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
</div>
)
}
// ,(prev, next) => { // memo 的第二个参数前后两种状态,如果相等组件不会更新【+1按钮】,如果不相等组件更新【GET CURRENT TIME】
// console.log(prev,next)
// console.log(prev.count === next.count)
// return prev.count === next.count // 和不写效果一样
// }
)
export { Parent }
传递复杂类型的值
const Parent = () => {
const [count, setCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
// 将需要传递给子组件的数据,包裹在一个对象中。
const timeOption = {
clickCount
}
return (
<div>
count: {count}
<button onClick={()=>{
setCount(count+1)
}}>+1</button>
<button onClick={() => {
setClickCount(clickCount + 1)
}}>GET CURRENT TIME</button>
{<!-- 给子组件传递一个复杂类型的数据 -->}
<Child count={timeOption}/>
</div>
)
}
const Child = memo((props) => {
console.log(123)
const date = new Date()
return (
<div>
<p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
</div>
)
}
export { Parent }
如果向子组件传递的是一个对象类型的数据,虽然父组件中clickCount没有发生改变, 但每次父组件中的clickCount没有发生改变但是timeOption对象每次都会重新生成。
即使子组件使用了memo,由于接收到的是对象类型的数据,由于对象是引用类型, 在子组件中得到的prev.count next.count的地址不同也会造成子组件的更新。
解决:
- 方法一:可以使用prev.count.clickCount 和 next.count.clickCount来return(因为对象里的这两个值是相同的,且是简单数据类型)
- 方法二:使用useMemo()
useMemo
useMemo 接收两个参数,第一个参数为回调,第二个参数为要依赖的数据。 在useMemo的回调中return一个数据结果,useMemo 会将return 的结果进行缓存。
const Parent = () => {
const [count, setCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
const timeOption = useMemo(() => {
return {clickCount}
}, [clickCount])
return (
<div>
count: {count}
<button onClick={()=>{
setCount(count+1)
}}>+1</button>
<button onClick={() => {
setClickCount(clickCount + 1)
}}>GET CURRENT TIME</button>
<Child count={timeOption}/>
</div>
)
}
const Child = memo((props) => {
console.log(props)
console.log(123)
const date = new Date()
return (
<div>
<p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
</div>
)
}
)
此时:在父组件中的clickCount没有发生改变, 使用useMemo()可以将这个结果缓存, 在父组件更新状态时不会每次都重新生成timeOption, 因此在memo()的第二个参数中通过比较前后两次的props是相同的, 从而不会触发子组件的更新。
useCallback
useCallback 接收两个参数,第一个参数为回调,第二个参数为要依赖的数据。
useCallback 计算结果是 函数, 主要用于 缓存函数。
// 当在子组件的输入框中输入内容时,在父组件中,会改变父组件中的text属性,并重新渲染父组件。
// 因此会重重复向子组件传递handleOnChange,在子组件中会重复打印props.
// 当使用useCallback时,向子组件传递的handleOnChange是同一个事件,
// 因此会被缓存,在子组件中不会重复打印父组件传递的方法
const Parent = () => {
const [text, setText] = useState('')
const handleOnChange = useCallback((e) => {
setText(e.target.value)
}, [])
return (
<div>
<p>text: {text}</p>
<Child onChange={handleOnChange}/>
</div>
)
}
const Child = memo((props) => {
console.log(props)
console.log(123)
return (
<div>
<input type="text" onChange={props.onChange} />
</div>
)
}
)
export { Parent }
总结
一个 state 的变化整个组件都会被重新刷新,一些函数和数据是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
memo、useMemo、useCallback都能用来提高性能,它们的主要区别是:
- memo 实现组件状态改变前后props是否发生改变,决定组件是否重新加载。它站在整个组件上。
- useMemo 实现属性的缓存,针对某一个属性的改变,确定子组件是否重新加载,而且它的返回值是一个数值。
- useCallback 实现方法的缓存,针对某一个方法的改变,确定子组件是否重新加载,它的返回值是一个方法。
useReducer useContext
useReducer useContext
使用 useReducer useContext实现组件之间的数据传递。
import React, { useReducer, useContext } from 'react'
// 创建全局上下文
const Ctx = React.createContext(null)
// 创建reducer,接收action并更改状态
const reducer = (state, action) => {
switch (action.type) {
case 'ADD':
return state + 1;
case 'SUB':
return state - 1;
default:
return state;
}
}
const Child = () => {
//如果在子组件中引入useReducer(),可以供子组件使用并修改子组件中的状态
//const [count, dispatch] = useReducer(reducer, 10)
//接收上下文中传递的count,dispatch.并在子组件中通过dispatch发送action
const [count, dispatch] = useContext(Ctx)
return (
<div>
child:
count: {count}
<button onClick={() => {dispatch({type: 'ADD'})}}>+1</button>
<button onClick={() => {dispatch({type: 'SUB'})}}>-1</button>
</div>
)
}
const Parent = () => {
//父组件接收子组件更新之后的状态
const [count] = useContext(Ctx)
return (
<div>
parent:{count}
<Child />
</div>
)
}
function App1() {
// 声明一个状态count,并附初始值为20,
// 并声明发送action的方法dispatch,
// 同时指明处理action的reducer
const [count, dispatch] = useReducer(reducer, 20)
return (
// 使用创建的上下文中的Provider,指定上下文的范围,
// 同时传递count状态和发送action的dispatch方法,以供子组件使用
<Ctx.Provider value={[count, dispatch]}>
<div className="App">
<Parent />
</div>
</Ctx.Provider>
);
}
export default App1;
自定义Hooks
自定义hooks
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
import { useEffect, useState } from 'react';
export const useWindowResize = () => {
const [width, setWidth] = useState('0px')
const [height, setHeight] = useState('0px')
useEffect(() => {
setWidth(document.documentElement.clientWidth + 'px')
setHeight(document.documentElement.clientHeight + 'px')
}, [])
useEffect(() => {
const handleResize = () => {
setWidth(document.documentElement.clientWidth + 'px')
setHeight(document.documentElement.clientHeight + 'px')
}
window.onresize = handleResize
return () => {
window.onresize = null
}
}, [])
return [width,height]
}
import React from 'react'
import { useWindowResize } from './hooks'
const Parent = () => {
const [width, height] = useWindowResize() // 也可以传递参数,
return(
<div>
<p>size: {width}*{height}</p>
</div>
)
}
更多 Hooks
- react-redux中提供的:
useSelectoruseDispatch - react-router-dom中提供的:
useHistoryuseLocationuseParamsuseRouteMatch