使用react-hooks及在项目中使用react-hooks遇到的一些问题的解决
什么是Hook?
Hook是React16.8的新增特性。它可以让我们在不编写class的情况下使用state以及其他react的特性。
Hook 是一些可以让我们在函数组件里“钩入” React state 及生命周期等特性的函数。他可以让我们不使用 class 也能使用 React。
先看一下类组件和函数式组件在实现同一个功能时,两者的区别。
class组件
import React, { Component } from “react”;
export default class Button extends Component {
constructor() {
super();
this.state = { buttonText: “Click me, please” };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(() => {
return { buttonText: “Thanks, been clicked!” };
});
}
render() {
const { buttonText } = this.state;
return {buttonText};
}
}
hooks函数式组件
import React, { useState } from “react”; // 引入useState
export default function Button() {
const [buttonText, setButtonText] = useState(“Click me, please”); // 调用 useState 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。
// const [loading, setLoading] = useState(false) 如果需要多个变量,可以通过调用多次useState来声明
function handleClick() {
return setButtonText(“Thanks, been clicked!”); // 用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Button 组件,并把最新的 值 传给它。
}
return {buttonText};
}
对比两种组件,以看出class组件的代码相对于函数式组件显得很笨重,层级很多,如果再加入redux的话就会变得更加复杂。
hooks常用的钩子:
1.状态钩子 useState
2.副作用钩子 useEffect
3. 共享状态钩子 useContext (我没用过。。。)
4. useCallback和useMemo
5.useRef
useState:
用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。
(1)useState()这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。该函数返回一个数组,数组的第一个成员是一个变量(上例是buttonText),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是set前缀加上状态的变量名(上例是setButtonText)。
(2)如果需要多个变量,可以通过调用多次useState来声明。
(3)需要注意,useState是异步的 ,如果需要处理异步,可以使用useEffect来监听state是否发生变化,从而进行操作。(具体事例见后useEffect)。
useEffect:
用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在componentDidMount里面的代码,现在可以放在useEffect()。
useEffect(() => {
const params = {
regionId: regionId || -1
}
http
.get(urls.API + ‘/QueryCondition/AuthCity/ByRegion’, params)
.then((res) => {
const { isSuccess, message, result } = res.data
if (!isSuccess) {
Message.error(message)
return
}
const dataList = props.noAll ? [] : [{ value: -1, label: ‘全部’ }]
if (result !== null) {
result.map((item) => {
if (item.name !== null) {
dataList.push({ value: item.id, label: item.name })
}
return item
})
}
setList(dataList || [])
setInitialValue(77 || null)
})
}, [regionId, props.noAll]) // 如果不传,每次组件渲染都会执行该useEffect
(1)上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()。
上例是我调用的一个获取城市的接口的副作用钩子,当大区变化时,就会触发该副作用。
(2)解决异步获取数据的问题
useEffect(() => {
if (checkedValue !== null || inputValue.length > 1) {
getSearchList()
}
}, [checkedValue, getSearchList, inputValue])
这里是我项目中写的一段通过查询条件获取查询列表的代码,直接获取checkedValue(单选框的值)和inputValue(输入框的值)当做参数给到接口是没有用的,我通过副作用hook来判断,当两者任意一个刷新时,执行查询方法。
useMemo:
使用hook时,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。这里我们使用useCallback和useMemo可以优化性能问题。
(1)useCallback和useMemo的参数跟useEffect一致。
(2)useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
反例:
import React from ‘react’;
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState(’’);
function expensive() {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
这里创建了两个state,然后通过expensive函数,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState(’’);
const expensive = useMemo(() => {
console.log(‘compute’);
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
上面我们可以看到,使用useMemo来执行计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。
useCallBack:
(1)useCallback跟useMemo比较类似,但它返回的是缓存的函数。
(2)使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
遗留问题: 由于useEffect、useMemo、useCallback都是自带闭包的。每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,不像class组件中,有componentDidUpdate(),我们是无法使用它们来捕获上一次的状态。(不像class组件中,有componentDidUpdate())
注意:useEffect,useMemo,useCallback都有一个坑点,他们的第二个参数,就是那个用来放涉及的变量的数组,有的变量是不可以放在这个数组中的,一旦加了就会造成死循环。但是你不加上,eslint就会警告(React Hook useEffect has missing dependencies: ‘mockData’, and ‘targetKeys’. Either include them or remove the dependency array),逼死强迫症。我这边的处理方式如下:
const getSearchList = useCallback(
(): void => {
const params = {
name: inputValue.length > 1 ? inputValue : undefined,
type: checkedValue
}
const oldMockData = mockData.filter((item) => {
for (let i = 0; i < targetKeys.length; i++) {
if (item.key === targetKeys[i]) {
return item
}
}
})
http
.get(urls.CARAPI + ‘/CarRepair/RepairItem’, params)
.then((res: any) => {
const { isSuccess, message, result } = res.data
const data = []
if (!isSuccess) {
Message.error(message)
return
}
result.map((item) => {
data.push({
value: item.id,
label: item.name,
key: item.id,
count: 1,
material: 0,
hourFee: 0,
serviceTypeName: item.serviceTypeName,
serviceTypeId: item.serviceTypeId
})
return item
})
setMockData([…data, …oldMockData])
})
},
// eslint-disable-next-line
[checkedValue, inputValue]
)
用 //eslint-disable-next-line 让eslint忽略下一行的校验,暂时只找到这个方法,有其他的方法可以教教我。
useRef:
react hooks 用来获取组件实例对象或者是DOM对象的钩子。
在我们实际开发中,经常会遇到父组件想要获取子组件的实例上的某些变量或者方法。class组件提供了createRef,useRef则是提供给函数式组件的钩子。
父组件片段:
const OutRepairAdd: React.FC = () => {
const tranRef: any = useRef()
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [carInfo, setCarInfo] = useState([] as carInfoParams)
const [imageList, setImageList] = useState([])
const [showBtn, setShowBtn] = useState(false)
// 获取子组件ref上的参数
const box = tranRef.current.checkedBox
handleAdd = () => {
this.setState({
applyLoading: true
})
applyRef.current.handleApply()
setTimeout(() => {
this.afterQuery()
}, 1500)
}
// 子组件
子组件片段:
interface transferProps {
[propName: string]: any
}
const TableTransfer = forwardRef((props: transferProps, ref: any) => {
const [checkedBox, setCheckedBox] = useState([])
const [checkedValue, setCheckedValue] = useState(null)
useImperativeHandle(ref, () => ({
checkedBox,
// handleApply 是暴露给父组件的提交方法
handleApply: async () => {
const data = {
id: props.id,
status:
radioValue === 1
? new Big(approvalStatus).plus(1).toNumber()
: radioValue,
oprNo: String(userId),
remark: remarkValue,
orderItems: approvalStatus === 2 ? tableList : null
}
await http
.put(urls.CARAPI + ‘/CarRepairOuter/Approval’, ‘’, data)
.then((res) => {
const { isSuccess, message, result } = res.data
if (!isSuccess) {
Message.error(message)
props.closeLoading()
return
}
if (!result) {
Message.error(message)
props.closeLoading()
}
props.closeLoading()
props.onCancel()
Message.success(‘审批完成’)
})
}
}))
(1)上例中,子组件引用了一个useImperativeHandle方法,useImperativeHandle 可以让我们在使用 ref 时自定义暴露给父组件的实例值
(2)useImperativeHandle 应当与 forwardRef 一起使用。
useContext:
如果需要在组件之间共享状态,可以使用useContext()。
上一篇: webpack4打包后的代码分析