0.前言
目前react已全面拥抱hook,但使用hook进行开发时,你会发现state的值往往跟你想象的不一样,为什么state会这么奇怪呢,通过以下案例,让我们一探究竟吧。
1. state类型为Object或Array时,setState无法生效。
当我们state所定义的state类型为Object或Array时,在回调中直接setState是无法成功的,demo如下:
function App() {
const [obj,setObj] = useState({
num:1
});
const clickMe = () => {
setObj(v => {
let newObj = v
newObj.num = v.num + 1 // 直接修改num的值不成功
return newObj
})
}
return (
<button onClick={clickMe}>{obj.num}</button>
);
}
样例——此时num的值一直为1。
由于Object为引用类型,setState通过回调函数的形式赋值,其参数v存的是obj的地址,此时let newObj = v操作将newObj指向obj的地址,因此newObj.num = v.num + 1这个操作相当于v.num = v.num +1。
由于v是引用类型,直接返回原对象时,react无法检测到值的变化,视图因此无法成功更新。
通过浅拷贝或者深拷贝(相关资料网上很多),重新定义一个新的对象,可解决此问题,将代码修改如下:
function App() {
const [obj,setObj] = useState({
num:1
});
const clickMe = () => {
setObj(v => {
let newObj = Object.assign({},v) // 对v进行浅拷贝
newObj.num = v.num + 1
return newObj
})
}
return (
<button onClick={clickMe}>{obj.num}</button>
);
}
样例此时newObj指向一个新的拷贝对象,可以任意修改newObj值,原值保持不变。
2. setState后值未立即发生改变
修改state后,如果直接调用此state,你会发现state的值未发生改变,demo如下:
function App() {
const [num,setNum] = useState(0);
const clickMe = () => {
setNum(num+1)
console.log(num)
}
return (
<button onClick={clickMe}>{num}</button>
);
}
此时点击button,第一次button显示的num值变为1,而后台的num值显示为0,多次点击,后台总比视图要少1。
与react的更新有关,当调用setState时,react是异步更新state的,如果setState后立即获取state的值,此时state尚未更新,因此为旧的状态。
修改state的同时需要使用state的值时,建议使用函数的方式修改并进行相关的使用操作,将上面的方法修改如下:
function App() {
const [num,setNum] = useState(0);
const clickMe = () => {
setNum(num => {
let newVal = num + 1
console.log(newVal)
return num+1
})
}
return (
<button onClick={clickMe}>{num}</button>
);
}
3. 异步获取的state值不是最新的state的值
当我们在一个异步函数中获取state值时,如果异步未执行完成时修改这个state的值,异步结束后获取的值仍然为原来的值,具体demo如下:
function App() {
const [num, setNum] = useState(0);
const clickMe = () => {
setTimeout(() => alert(num), 2000);
};
return (
<>
<button onClick={clickMe}>click me</button>
<input
onChange={e => {
setNum(e.target.value);
}}
/>
</>
);
}
样例——在输入框先输入一组数字,点击click me后再输入几个数字,弹出的信息为click时的数字。
这是由于函数组件中state是闭包的,因此每次调用函数获取的state只与当时的值有关(为什么要这样设计可查看dan的文章:函数式组件与类组件有何不同?)。设想如果setTimeout是一个请求,那么请求成功后我们想要的应该是调用这个函数时的state,但有时候我们就是需要修改后的state,所以我们要使用其他方法去获取这个值。
通过useRef获取当前值,useRef 返回一个可变的 ref 对象,num变化时修改numRecent.current的值,可将numRecent的值保持最新状态。
function App() {
const [num, setNum] = useState(0);
const numRecent = useRef('');
const clickMe = () => {
setTimeout(() => alert(numRecent.current), 2000);
};
return (
<>
<button onClick={clickMe}>click me</button>
<input
onChange={e => {
numrecent.current = e.target.value;
setNum(e.target.value);
}}
/>
</>
);
}
样例-此时state始终与视图保持一致。
4.利用通用方法避坑
实际开发中会经常遇到如上几个问题,通过setState修改状态的同时需要根据新的状态进行一些操作,比如进行请求,修改obj的结构等,每次都要进行拷贝操作会让代码显得冗余,状态不一致性也让人头痛,因此建议将其简单封装为一个通用函数,具体如下:
const setState = (newState,changeStateFn, callback) => {
changeStateFn((state) => {
if(state.constructor === Object) {
state = Object.assign({},state,newState)
}
if(state.construct === Array) {
state = newState.slice()
}
callback(state)
return state
})
}
然后修改第1部分的方法如下:
const clickMe = () => {
setState({num:obj.num+1},setObj,(v) =>{
console.log(v.num)
})
}
return (
<button onClick={clickMe}>{obj.num}</button>
);
是不是清晰了很多呢?
附完整代码:
import React, { Component,useState } from 'react';
import { render } from 'react-dom';
function App() {
const [obj,setObj] = useState({
num:1
});
const setState = (newState,changeStateFn, callback) => {
changeStateFn((state) => {
if(state.constructor === Object) {
state = Object.assign({},state,newState)
}
if(state.construct === Array) {
state = newState.slice()
}
callback(state)
return state
})
}
const clickMe = () => {
setState({num:obj.num+1},setObj,(v) =>{
console.log(v.num)
})
}
return (
<button onClick={clickMe}>{obj.num}</button>
);
}
render(<App />, document.getElementById('root'));
5. 总结
以上都是开发中经常遇到的问题,希望能够帮到大家,如果对您有帮助,还请帮忙点个赞呦。