本篇是个人笔记,并非教程,建议看原文,虽然比较长,但看个两三遍试用一下还是非常有提升的,有问题可以一起交流~
原文地址:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
作者简介:https://overreacted.io/zh-hans/my-decade-in-review/
useEffect(fn, [])和componentDidMount的区别
前者会捕获props和state,所以在回调函数中不管等多久后,拿到的props和state依旧是初始值(定时器例子)。如果需要拿到实时数据,则可使用ref或后文提到的其它方式。
函数存放的最佳位置
- 和state和props无关,放到组件外
- 只和某个useEffect有关,放到这个里面
- effect使用到以上两种之外的函数,用useCallback包裹起来。
每次渲染
- state和props是每帧完全独立的,对于本次的渲染来说就是个常量,包括对象类型,当然我们不能放弃setState而直接修改对象的属性
函数也会每帧独立渲染,被调用时传入的参数就是当前帧的数值,不会随着组件更新而变化。 - 都有它自己的Effects,即Effects内部保存了当次渲染时的state和props,可以理解为effects是组件渲染的一部分。
effects运行时机
React只会在浏览器绘制后运行effects。
延时的情况
- 如果effect中有延时,则执行时输出的state和props仍然是当初调用时的值,不会随着组件更新而改变(原理是闭包);但在class组件的didUpdate里却随时更新。即如下两种写法等价
function Example(props) {
useEffect(() => {
setTimeout(() => {
console.log(props.counter);
}, 1000);
});
// ...
}
function Example(props) {
const counter = props.counter;
useEffect(() => {
setTimeout(() => {
console.log(counter);
}, 1000);
});
// ...
}
useRef 在effect中使用实时的state和effect
如下实现相当于模拟了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中的清除
effects的return:每次浏览器渲染完后运行effects,首先调用return中定义的函数进行一些清除工作,而函数里的state或props是上一次的值,即定义时保存的数据。假设每次订阅一个id再清除这个id,如果第一次id为10,第二次id为20,则本以为react的执行顺序是:
- 清除id为10的effect
- 渲染id为20的UI
- 运行id为20的effect
而事实上是
- 渲染id为20的UI
- 清除id为10的effect
- 浏览器绘制。我们在屏幕上看到{id: 20}的UI。
- 运行id为20的effect
即先渲染本帧后清除上一帧。
useEffect 更新条件
提供给useEffect一个依赖数组参数(deps),相当于告诉react这个effect只用到了这些参数,当组件更新时所有参数都没有变化,react就会跳过执行此次effect。如果有一个参数变化,则此次会同步所有参数。
如何写更新条件?
“将诚实地告知effect依赖作为一条硬性规则,并且要列出所以依赖”
- 第一种策略是在依赖中包含所有effect中用到的组件内的值。
- 第二种策略是修改effect内部的代码以确保它包含的值只会在需要的时候发生变更。
setState的函数形式
当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
useReducer
一个原则:只在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;
来自于props的step
如果上例中的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
// ...
}
- 使用useCallback以及将函数作为依赖。
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
// ...
}
- 如果query是个其他地方可以改变的组件内state,则可以设置为依赖
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
// ...
}
- 如果函数是从父组件传入,比如子组件发现父组件的query改变后就去请求数据,仍然适用于上面的方法
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是参与数据流的,就能更好的解决这个问题。
- 类似的,使用useMemo可以对复杂对象做更多的事情,下面例子中使用useMemo封装了一个dom需要的stlye格式,只要color改变,dom就跟随渲染了。
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} />;
}
- 需要注意点是,作者说 “到处使用useCallback是件挺笨拙的事。在上面的例子中,我更倾向于把fetchData放在我的effect里(它可以抽离成一个自定义Hook)或者是从顶层引入。我想让effects保持简单,而在里面调用回调会让事情变得复杂” 。
竞态问题
定义:“请求结果返回的顺序不能保证一致。比如我先请求 {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。