本篇是个人笔记,并非教程,建议看原文,虽然比较长,但看个两三遍试用一下还是非常有提升的,有问题可以一起交流~
原文地址:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
作者简介:https://overreacted.io/zh-hans/my-decade-in-review/
前者会捕获props和state,所以在回调函数中不管等多久后,拿到的props和state依旧是初始值(定时器例子)。如果需要拿到实时数据,则可使用ref或后文提到的其它方式。
React只会在浏览器绘制后运行effects。
function Example(props) {
useEffect(() => {
setTimeout(() => {
console.log(props.counter);
}, 1000);
});
// ...
}
function Example(props) {
const counter = props.counter;
useEffect(() => {
setTimeout(() => {
console.log(counter);
}, 1000);
});
// ...
}
如下实现相当于模拟了class中的行为:
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);
});
// ...
effects的return:每次浏览器渲染完后运行effects,首先调用return中定义的函数进行一些清除工作,而函数里的state或props是上一次的值,即定义时保存的数据。假设每次订阅一个id再清除这个id,如果第一次id为10,第二次id为20,则本以为react的执行顺序是:
而事实上是
即先渲染本帧后清除上一帧。
提供给useEffect一个依赖数组参数(deps),相当于告诉react这个effect只用到了这些参数,当组件更新时所有参数都没有变化,react就会跳过执行此次effect。如果有一个参数变化,则此次会同步所有参数。
如何写更新条件?
“将诚实地告知effect依赖作为一条硬性规则,并且要列出所以依赖”
当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
一个原则:只在effect中传递最小的信息,如上。有时上述方法并不能完全解决问题,比如下面的例子中step改变时count会更新步长,定时器被清除后会重启。
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。
当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。当你写类似setSomething(something => …)这种代码的时候,也许就是考虑使用reducer的契机。reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。
我们用一个dispatch依赖去替换effect的step依赖:
import React, {
useReducer, useEffect } from 'react';
const initState = {
count: 0,
step: 1
}
const 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 }
}
}
const Reducer01 = () => {
const [state, dispatch] = useReducer(reducer, initState)
const {
count, step} = state
useEffect(() => {
const t = setInterval(() => {
dispatch({
type: 'tick' })
}, 1000)
return ()=>[
clearInterval(t)
]
}, [dispatch]) // 实际上,这个依赖可以不写,因为dispatch在整个组件的生命周期中是不变的
return (
<div>
<h3>使用reducer</h3>
<p>{
count}</p>
<input onChange={
e=>dispatch({
type:'step',step:Number(e.target.value)})} />
</div>
);
}
export default Reducer01;
如果上例中的step来自于props,则可以把reducer写到组件内获取step。但这种模式会使一些优化失效,所以应该避免滥用——reducer随着每次组件渲染会生成一份新的。useReducer可以理解为是Hooks的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。好处是,这可以帮助我移除不必需的依赖,避免不必要的effect调用。
const Reducer02 = ({
step }) => {
const reducer = (state, action) => {
const {
count } = state
if (action.type === 'tick') {
return {
count: count + step, step }
} else if (action.type === 'step') {
return {
count, step: action.step }
}
}
const [state, dispatch] = useReducer(reducer, initState)
const {
count } = state
useEffect(() => {
const t = setInterval(() => {
dispatch({
type: 'tick' })
}, 1000)
return () => [
clearInterval(t)
]
}, [])
return (……);
}
关于获取数据的一个例子:查询参数query改变时,useEffect自动执行拉取数据的操作。
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect(() => {
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
// ...
}
当上述例子中需要复用getFetchUrl逻辑时,将其写在useEffect外面的组件内,同时将函数作为依赖,这样会造成每次刷新都会请求数据。
function SearchResults() {
// Re-triggers all effects on every render
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]); // Deps are correct but they change too often
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]); // Deps are correct but they change too often
// ...
}
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
// ...
}
function SearchResults() {
// ✅ Preserves identity when its own deps are the same
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
function SearchResults() {
const [query, setQuery] = useState('react');
// ✅ Preserves identity until query changes
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
function Parent() {
const [query, setQuery] = useState('react');
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query; // ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={
fetchData} />
}
function Child({
fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}
需要注意的是这种情况不能放入class组件中使用,因为函数在整个生命周期中只有一份,地址永远不变,无法驱动query变动后的刷新。解法是封装一个子组件去请求数据,然后把query传给子组件,在子组件中判断query来决定是否要更新。此时query就相当于只为了让子组件做diff才传入的,而在hooks中,effect是参与数据流的,就能更好的解决这个问题。
function ColorPicker() {
// Doesn't break Child's shallow equality prop check
// unless the color actually changes.
const [color, setColor] = useState('pink');
const style = useMemo(() => ({
color }), [color]);
return <Child style={
style} />;
}
定义:“请求结果返回的顺序不能保证一致。比如我先请求 {id: 10}时的数据,然后更新到{id: 20},但{id: 20}的请求更先返回。请求更早但返回更晚的情况会错误地覆盖状态值。” 解决的方法是使用一个布尔值跟踪状态。
function Article({
id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
// 当这一步等待时间长未返回数据时下一次请求发生了,
// 则在effect清除工作中会置didCancel为true,
// 即使这次结果返回了也会被丢弃,不再影响到当前的数据
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
// ...
}
本文讲述的effect基本都是初级使用水平,而社区将会推出一些基于effect的hooks,以减少我们的频繁手动创建effect。