React的useEffect的正确使用姿势
这篇文章是在阅读了Dan神的UseEffect完整使用指南之后写下的一个个人理解,这是一篇很好的文章,而且对初学者相对友好,如果感兴趣强烈建议阅读原文。
开始正题:React的useEffect副作用函数
自从React推出了函数组件的各种hooks之后赢得很多人的青睐,可是最常用的一个钩子函数UseEffect对于很多初学者并不友好,很多人都把他当作类组件的生命周期钩子函数来使用。的确,它在某种程度上可以说是生命周期钩子的替代,可是也不完全是,具体在于他们的执行时机和依赖项上。下面来看看他们的区别。
React的组件更新中:useEffect 和 类组件状态的区别
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
第一种情况:useEffect相当于每个函数自己的作用域,每当setCount一次,浏览器挂在完此刻状态的UI后会执行useEffect(因为依赖项是没写的),所以每一次打印都是拿到属于当前状态的count
第二种情况:类组件总是指向于最新的count,他不在乎你点了多少次,三秒钟之内即使你点了无数次,他还是打印最新的值
第三种情况:如果我想改造函数组件成类组件那样每一次都拿到的是最新的值,我们怎么做?最简单的方法是useRef
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
//每一次执行定时器之前,给ref赋值此刻count(最新的状态)
latestCount.current = count;
setTimeout(() => {
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
如何解决每一次重新选然后都重复执行useEffect?使用deps!
1、React只会更新DOM真正发生改变的部分,而不是每次渲染都大动干戈。
//例如把:
<h1 className="color">
green
</h1>
//更新成:
<h1 className="color">
red
</h1>
//react:可以看到两个对象:
const oldProps = {className: 'color', children: 'green'};
const newProps = {className: 'color', children: 'red'};
//react会检测每一个props,只有children发生了改变需要更新DOM节点,但是类名是没有改变的,所以react只会做
document.xxx.innerText = 'red'
2、正确的使用依赖项
因为React看不到函数里面的东西,所以我们要给React一个数组,当re-render时,数组里面的东西有一个发生改变了,就要重新执行useEffect
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]);
(依赖发生了变更,所以会重新运行effect。)
但是如果我们将[]
设为effect的依赖,新的effect函数不会运行:
useEffect(() => {
document.title = 'Hello, ' + name;
}, []);
(依赖没有变,所以不会再次运行effect。)
经典题目
function Counter() {
const [count, setCount] = useState(0);
console.log('count:', count);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
这么写,最大的问题就在于:useEffect的deps是空数组,re-render之后对比发现deps是没有变化的,re-render就直接跳过了此次useEffect的执行,所以每一秒后都会一直执行setCount(0+1)这个操作,但由于所有东西(节点)都没变,所以只会打印 count:1 一次
解决方法:
1、用正确的deps
每一次,count都会发生变化,那么在下一次re-render挂在完DOM之后,执行useEffect时,会先执行上一次useEffect返回的函数,但这么做就相当于每一次re-render都重新定义一个循环定时器,这操作多少有点冗余,这是一种解决方案,但不够优雅!
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
2、用一些移除依赖的技巧
在我们需要移出某些依赖的时候,我们先考虑一下,为什么会用到这些依赖?这个场景下是因为我们要setCount(count + 1),所以count就成了必需品,可是我们想做的操作不过是把count基于当前的状态不断地累加而已。这里我们可以用setState的函数的方式。因为setState((prevalue)=>{}),这种方式,React是知道当前的状态(count)的,我们需要做的只是一直调用这句话。
useEffect(() => {
const id = setInterval(() => {
setCount(prevalue => prevalue + 1);
}, 1000);
return () => clearInterval(id);
}, []);
3、用加强版的useState:useReducer
当需要一个状态的更新依赖于另一个状态的时候,可以考虑一下useReduccer
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
这样做,确实没毛病,可是就是每次重新输入累加的数值之后要重新清除掉定时器,不够优雅!可以尝试使用useReducer去进行替换。
const initState = {
count: 0,
step: 1
}
const countReducer = (state, action) => {
const {count, step} = state
switch (action.type) {
case 'count':
return {count: count + step, step}
case 'step':
return {count, step: action.payload}
default :
return new Error('错了')
}
}
function Counter() {
const [count, setCount] = useState(0);
const [state, dispatch] = useReducer(countReducer, initState)
console.log(state);
useEffect(() => {
const id = setInterval(() => {
dispatch({type: 'count'})
return () => {
clearInterval(id)
}
}, 1000)
}, [])
//你可以从依赖中去除dispatch, setState, 和useRef包裹的值因为React会确保它们是静态的。不过你设置了它们作为依赖也没什么问题。
const handleInput = (e) => {
dispatch({
type: 'step',
payload: Number(e.target.value)
})
}
return <div>
<h2>{state.count}</h2>
<input type="text" placeholder={'需要递增的值'} onInput={handleInput}/>
</div>
}
假如我们需要依赖props去计算下一个状态呢?举个例子,也许我们的API是<Counter step={1} />
,我们可以把reducer函数放到组件内去读取props:
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === 'tick') {
return state + step;
} else {
throw new Error();
}
}
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
这里可能有个疑惑:第一次开启了循环定时器,dispatch之后,为什么之前定义的reducer函数可以知道新的props?原因是dispatch的时候,React只记住了action那个对象,然后重新执行整个Counter组件函数,重新定义reducer函数,所以里面可以访问到新的props。