2023 年了~useEffect 你真的会用嘛
前言
这篇文章是我依据 a -complete-guide-to-useeffect 本人总结进去的一些重点~
依照程序顺次如下:
- React 单向数据流的渲染
- Effect 的执行机会
- 不要对 Effect 扯谎:依赖数组要怎么设置
- 应用
setState
和useReducer
将上报行为和状态更新解耦 - 函数是否能够作为 Effect 的依赖呢?
同时咱们能够在浏览的时候时刻问本人以下问题:
- 🤔 如何用 useEffect 模仿 componentDidMount 生命周期?
- 🤔 如何正确地在 useEffect 里申请数据?[]又是什么?
- 🤔 我应该把函数当做 effect 的依赖吗?
- 🤔 为什么有时候会呈现有限反复申请的问题?
- 🤔 为什么有时候在 effect 里拿到的是旧的 state 或 prop?
每一次渲染都有它本人的 props 和 state
function Counter() {const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
+ <button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
上述例子中,对于 <p>You clicked {count} times</p>
中的count
,可能 react 的初学者会认为其是响应式的,也就是会主动监听并且更新渲染。
但实际上,它只是一个一般的变量 count
,没有任何的data bounding
。咱们在每一次调用setCount
的时候,实际上是 React 应用新的 count
值来重复渲染组件。第一次是 0,第二次是 1,以此类推。。。
除了变量,那么事件处理函数呢?
function Example() {const [count, setCount] = useState(0);
function handleAlertClick() {setTimeout(() => {alert('You clicked on:' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
咱们设想一下:
- 首先间断点击 Click me 两次
- 再点击一次 Show alert
- 持续点击 Click me
在点击 alert 的 3 秒过后,其显示的值会是什么呢?
答案是 2!!,这也证实了咱们上述的论断。即事件处理函数跟变量一样,也是独立属于以后的渲染。
每次渲染都有它独立的 Effect
咱们来看看官网上的例子:
function Counter() {const [count, setCount] = useState(0);
useEffect(() => {document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
这里依据之前的学习咱们晓得,并不是「不变的 Effect 中 count
的值发现了变动,而是 Effect 每一次渲染都不雷同,每一次 Effect 都捕捉了以后渲染的 count
值」
<div style=”color:#d2568c;border-left:5px solid;padding:6px”><span style=”font-weight:bold;”>「React 会记住你提供的 effect 函数,并且会在每次更改作用于 DOM 并让浏览器绘制屏幕后去调用它。」</span></div>
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>
);
}
咱们来思考一下上述代码,咱们给每一次渲染都增加一个延时函数。当咱们疯狂点击 Click me 的时候,会产生什么呢?
答案是:「程序打印 0,1,2,3…」
当然,你可能会想,如果我就是想要获取最新的值要怎么办的?应用useRef
!
function Example() {const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
此时当咱们疯狂点击后,最终打印进去的全都是 最新的 count
值。
Effect 中的清理又是怎么样的?
思考官网的案例:
useEffect(() => {ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
假如第一次渲染的时候 props 是{id: 10}
,第二次渲染的时候是{id: 20}
。你可能会认为产生了上面的这些事:
- React 革除了
{id: 10}
的 effect。 - React 渲染
{id: 20}
的 UI。 - React 运行
{id: 20}
的 effect。
事实上并不是这样的。React 只会在浏览器渲染结束后才会去执行 Effect,这并不会阻塞渲染使得你的利用更加晦涩。
Effect 的清理实际上也被延后了。上一次 Effecet 的清理会在 UI 渲染完结后被执行。
- React 渲染
{id: 20}
的 UI。 - 浏览器绘制 咱们在屏幕上看到
{id: 20}
的 UI。 - React 革除
{id: 10}
的 effect。 - React 运行
{id: 20}
的 effect。
通知 react 如何去比对 useEffect
简略来说,你的 Effect 可能会因为其余状态变动了而造成了不必要的调用。
为了解决这个问题,能够通过 依赖数组 来通知 React
如果以后渲染中的这些依赖项和上一次运行这个 effect 的时候值一样,因为没有什么须要同步,React 会主动跳过这次 effect。
如果依赖项设置谬误会怎么样呢?
举个例子,咱们来写一个每秒递增的计数器。在 Class 组件中,咱们的直觉是:“开启一次定时器,革除也是一次”。
当咱们天经地义地把它用 useEffect
的形式翻译,直觉上咱们会设置依赖为[]
。因为“我只想运行一次 effect“。
function Counter() {const [count, setCount] = useState(0);
useEffect(() => {const id = setInterval(() => {setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
在这个例子中,·count
的值永远都不会超过 1
。 因为定时器只会设置一次,而此时的 count
永远都是 0,即永远都在执行setCount(0+1)
咱们必须扭转咱们的想法。所谓 「依赖数组」 是咱们用来通知 React 该 Effect 用到了什么状态。在这个例子中,咱们对 React 扯谎了,通知 React 说没有用到任何组件内的值。但实际上依赖了count
。
要诚恳的通知 React 咱们正确的依赖的两种方法
1. 将所有 Effect 用到的依赖都通知给 React
useEffect(() => {const id = setInterval(() => {setCount(count + 1);
}, 1000);
return () => clearInterval(id);
+ }, [count]);
当初依赖正确了,然而会有一个问题,就是每一次执行 setCount
的时候,都会导致计数器的革除和新建。这并不合乎咱们的初衷。
2. 批改 effect 外部的代码以确保它蕴含的值只会在须要的时候产生变更。
咱们不想告知谬误的依赖 – 咱们只是批改 effect 使得依赖更少。
持续看会下面的示例(将 count
传入依赖数组)。咱们须要思考,咱们为什么须要用到 count
?咱们想要通过setCount
来更新 count
的值。
但其实在这个场景,咱们并不需要获取 count
的值,咱们只须要获取上一次的状态进行解决即可。因而能够写成
useEffect(() => {const id = setInterval(() => {setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
函数式更新
对于 Effect 来说,咱们能够用 「同步」 的思维去了解。
以下面这个例子来说,同步的一个乏味的中央就是将同步信息和状态解耦。相似于 setCount(c => c + 1)
这样的更新模式比 setCount(count + 1)
传递了更少的信息,因为它不再被以后的 count
值“净化”。它只是表白了一种行为(“递增”)。
然而,setCount(c => c + 1)
并不完满,例如咱们有两个状态相互依赖,即下一次状态不仅仅依赖上一次状态,它就不能做到了。
这时候,useReducer
大哥就要出场了。
useReducer
咱们对下面例子做一些批改:
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))} />
</>
);
}
当初 count
的值不仅仅依赖本人上一次的值,还依赖 step
状态。
雷同的,目前咱们并没有对 React 说谎,因而它能够正确运行。但 问题在于 每一次扭转 step
后,计时器都会被销毁重建。
当你想更新一个状态,并且这个状态更新依赖于另一个状态的值 时,你可能须要用 useReducer
去替换它们。
当你写相似 setSomething(something => ...)
这种代码的时候,兴许就是思考应用 reducer 的契机。reducer能够让你把组件内产生了什么 (actions) 和状态如何响应并更新离开表述。
function Counter() {const [state, dispatch] = useReducer(reducer, initialState);
const {count, step} = state;
useEffect(() => {const id = setInterval(() => {dispatch({ type: 'tick'}); // 更新 count
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => {
// 更新 step
dispatch({
type: 'step',
step: Number(e.target.value)
});
}} />
</>
);
}
function reducer(state, action) {const { count, step} = state;
if (action.type === 'tick') {return { count: count + step, step};
} else if (action.type === 'step') {return { count, step: action.step};
} else {throw new Error();
}
}
应用 useReducer
能够完满解决上述问题。这是为什么呢?
答案在于 <span style=”color:#d2568c;font-weight:bold;”>「React 会保障 dispatch 在组件的申明周期内放弃不变」</span>。所以下面例子中不再须要从新订阅定时器。
(你能够从依赖中去除
dispatch
,setState
, 和useRef
包裹的值因为 React 会确保它们是动态的。不过你设置了它们作为依赖也没什么问题。)
与其在 Effect 中去获取状态,不如只是 dispatch 一个 action 来形容行为,这使得 Effect 与状态解耦,Effect 再也不必关怀具体的状态了~
应用 useRuducer 真的是在舞弊
当咱们的 step
是 props 传进来的又会怎么呢?咱们还是能够应用 useReducer
,但这种状况下咱们的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>;
}
有的小伙伴可能会问:上一次渲染的 reducer
怎么能获取到最新的 props 呢?
答案是:Effect 只是 dispatch 了行为 – 它会在下一次渲染中去调用 reducer,这是就能够获取到最新的 props 了。留神:「reducer 不是在 Effect 中调用的」
永远要记住 useEffect 的执行机会
useEffect 的执行机会在 Dom 渲染结束后。而且它是异步执行的,不会阻塞的。因而在上面例子中,会呈现页面闪动的成果。(0 -> 12 -> 2)
export default function FuncCom () {const [counter, setCounter] = useState(0);
useEffect(() => {if (counter === 12) {
// 为了演示,这里同步设置一个延时函数 500ms
delay()
setCounter(2)
}
});
return (
<div style={{fontSize: '100px'}}>
<div onClick={() => setCounter(12)}>{counter}</div>
</div>
)
}
换成了 useLayoutEffect
后,屏幕上只会呈现 0 和 2,这是因为 useLayoutEffect
的 同步个性 ,会在 浏览器渲染之前同步更新 react DOM 数据,哪怕是屡次的操作,也会在渲染前一次性解决完,再交给浏览器绘制。这样不会导致闪屏景象产生。
将函数放进 Effect 中
首先抛出问题:函数真的不应该成为 依赖项嘛?
咱们来看一个许多程序员都会写的一个案例:
function SearchResults() {const [data, setData] = useState({hits: [] });
async function fetchData() {
const result = await axios('https://hn.algolia.com/api/v1/search?query=react',);
setData(result.data);
}
useEffect(() => {fetchData();
}, []); // Is this okay?
// ...
}
该代码是能够失常运行的,然而它的拓展性十分差。试想一下,在我的项目代码一直增长下,咱们可能会拆散出很多个很长的函数,可能会有其余的依赖:
function SearchResults() {const [query, setQuery] = useState('react');
// Imagine this function is also long
function getFetchUrl() {return 'https://hn.algolia.com/api/v1/search?query=' + query;}
// Imagine this function is also long
async function fetchData() {const result = await axios(getFetchUrl());
setData(result.data);
}
useEffect(() => {fetchData();
}, []);
// ...
}
如果咱们遗记更新 Effect 的依赖数组的话,这不仅仅坑骗了 React,而且导致 Effect 不会同步所依赖的 state 和 props。
一个最简略的解决方案是,如果某些函数仅在 effect 中调用,你能够把它们的定义移到 effect 中:。这样的话程序员不须要思考这些“间接依赖”,而且在增加 query
状态的时候,意识到 Effect 依赖于它。
function SearchResults() {const [query, setQuery] = useState('react');
useEffect(() => {
// ❇️ get together
function getFetchUrl() {return 'https://hn.algolia.com/api/v1/search?query=' + query;}
async function fetchData() {const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();}, [query]); // ✅ Deps are OK
// ...
}
咱们必须意识到:·useEffect
设计用意就是要强制你去关注数据流的变动,而后决定怎么去让 Effect 同步!!
但我不能将函数放进 Effect 中
举个例子,比方某函数被多个 Effect 应用到了。
首先咱们要明确一点,在组件中定义的函数每一次渲染都是变动的,但这个事实也给咱们带来了问题。比方:
function SearchResults() {function getFetchUrl(query) {return 'https://hn.algolia.com/api/v1/search?query=' + query;}
useEffect(() => {const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []); // 🔴 Missing dep: getFetchUrl
useEffect(() => {const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []); // 🔴 Missing dep: getFetchUrl
// ...
}
一方面咱们不想在每一个 Effect 中都复制雷同的代码,但这又是对 React 的不诚实~,另一方面假如咱们将 getFetchUrl
当作依赖项,但它每一次渲染都在变,咱们的依赖数组就没有施展用途了。
这里有两个解决办法:
- 如果函数并没有用到组件内的值,咱们能够大胆地将其移到组件内部!
- 应用
useCallBack
hook 包裹。
useCallBack
实质上只是加了一层依赖查看,使得函数自身只有在须要时才扭转,这样咱们也不须要去掉函数依赖。
当函数用到了组件内的值,比方 query
能够通过输入框由用户去扭转,咱们能够这样做:
function SearchResults() {const [query, setQuery] = useState('');
const getFetchUrl = useCallback(() => {return 'https://hn.algolia.com/api/v1/search?query=' + query;}, [query]); // ✅ Deps are OK
useEffect(() => {const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Deps are OK
useEffect(() => {const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Deps are OK
// ...
}
这才是拥抱 React 数据流和同步思维的最终后果。query
扭转 -> getFetchUrl
扭转 -> Effect 从新执行。反之,如果 query
不变,则 Effect 将不会从新执行
函数是数据流的一部分吗?
乏味的是,这种模式在 class 组件中是行不通的。
class Parent extends Component {
state = {query: 'react'};
fetchData = () => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
// ... Fetch data and do something ...
};
render() {return <Child fetchData={this.fetchData} />;
}
}
class Child extends Component {
state = {data: null};
componentDidMount() {this.props.fetchData();
}
componentDidUpdate(prevProps) {
// 🔴 This condition will never be true
if (this.props.fetchData !== prevProps.fetchData) {this.props.fetchData();
}
}
render() {// ...}
}
在 class 组件中,类办法 this.fetchData
是永远不会变的
为了解决这个问题,咱们必须将 query
参数传给 Child
组件即便它并没有间接应用到query
:
class Child extends Component {componentDidUpdate(prevProps) {
// 🔴 This condition will never be true
if (this.props.query !== prevProps.query) {this.props.fetchData();
}
}
在 class 组件中,函数属性自身并不是数据流的一部分。组件的办法中蕴含了可变的 this 变量导致咱们不能确定无疑地认为它是不变的。因而,即便咱们只须要一个函数,咱们也必须把一堆数据传递上来仅仅是为了做“diff”。咱们无奈晓得传入的
this.props.fetchData
是否依赖状态,并且不晓得它依赖的状态是否扭转了。
然而感激useCallback
,它让函数自身也参加进了 React 数据流,咱们能够说函数的输出变了,那么函数自身就变了。(函数的输出须要咱们来定义)
🌟须要强调的是 :当咱们须要将函数传递给子组件并且在子组件的 Effect 中使用的话,最好应用useCallback
将其包裹。
对于竞态
上面是一个 class 组件发申请的案例
class Article extends Component {
state = {article: null};
componentDidMount() {this.fetchData(this.props.id);
}
async fetchData(id) {const article = await API.fetchArticle(id);
this.setState({article});
}
// ...
}
心细的小伙伴可能发现了,下面代码并没有思考到 id 更新的状况,于是:
componentDidUpdate(prevProps) {if (prevProps.id !== this.props.id) {this.fetchData(this.props.id);
}
}
然而这真的能够完满解决所有问题嘛?
咱们能够试想一下,比方我先申请 {id: 10}
,而后更新到 {id: 20}
,但{id: 20}
的申请更先返回。申请更早但返回更晚的状况会谬误地笼罩状态值。这就是「竞态」
最好的权宜之计就是利用一个 boolean 来去记录以后申请状态。
更多相干常识能够看这篇文章
结尾
我是 <span style=”color:#87481f;font-weight:bold”>「盐焗乳鸽还要香锅」</span>,喜爱我的文章欢送关注噢
- github 链接 https://github.com/1360151219
- 博客链接是 strk2.cn
- 掘金账号、知乎账号、简书《盐焗乳鸽还要香锅》
- 思否账号《天天摸鱼真的爽》
本文由 mdnice 多平台公布