我知道你想用useEffect,但你先别急
阿里妹导读
本文95%来自官方文档,均为官方推荐的写法或者做法。
你是不是也遇到过这个情况?
useEffect(()=>{
query();
},[name])
随着迭代,需求变更,name也会被其他地方给修改,但是他只是临时修改,并不需要发起一次请求。
useEffect(()=>{
if(isReFresh){
query();
}
},[name])
很好,没毛病,不会重复发起请求了。
useEffect(()=>{
if(isReFresh){
query();
}
},[name,age])
随着时间推移,你逐渐发现:
「是否发送请求」与「if条件」相关
「是否发送请求」还与「name、age等依赖项」相关
「name、age等依赖项」又与「很多需求」相关
Effect的生命周期
当组件被添加到屏幕上时 --- mount
当组件接收到新的props或者state时(通常是为了响应式交互) --- update
当组件从屏幕上被移除时 --- unmount
这是设计组件生命周期的好办法,但是并不适用于Effect。Effect只能做两件事情:
开始同步一些内容
稍后停止同步
为什么要少用Effect?
哪些情况可以不用Effect?
Don’t rush to add Effects to your components. Keep in mind that Effects are typically used to “step out” of your React code and synchronize with some external system. This includes browser APIs, third-party widgets, network, and so on. If your Effect only adjusts some state based on other state, you might not need an Effect. 不要急于向组件添加Effect。请记住,Effects 通常用于“跳出”React 代码并与某些外部系统同步,你可以把 Effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。这包括浏览器 API、第三方小部件、网络等。如果您的效果仅根据其他状态调整某些状态,则您可能不需要Effect。
一、依赖props或者state
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid:冗余的状态和不必要的Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
没必要这么写,既复杂又低效。这种情况就命中了我前面说的他会从头开始重新渲染整个页面。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: 重渲染期间计算
const fullName = firstName + ' ' + lastName;
// ...
}
当你需要计算某些值,而恰好计算的参数都是可以从Props或者State中获取的,不要把他放在State中,相对应的,可以在渲染期间计算它。这使得:
代码更快(避免了额外的“级联”更新)
更简单(删除了一些代码)
更不容易出错(避免了由于不同状态变量彼此不同步而导致的错误)
非state数据是否可以?
答:不可以
这个就涉及到React本身的更新渲染机制,也就是我们为什么要用useState来set数据。如果不用useState,是不会触发React本身的更新的。
二、缓存昂贵的计算
一般而言,除非您要创建或遍历数千个对象,否则它可能并不昂贵。
如果记录的总时间加起来很大(比如 1 毫秒或更多),那么记住该计算可能是有意义的。
可以利用如下的方式来测算:
console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 Avoid: 冗余的状态和不必要的Effect
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
与前面的示例一样,这既不必要又低效。
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Good:当getFilteredTodos执行的不慢的时候,这个写法很好
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
通常情况下,这种写法已经是很优秀的写法了!但是如果getFilteredTodos很慢,或者你有很多其他的待办事项会导致props或者state变化的非常的频繁。在这种情况下,如果你不想重新计算的话,可以将昂贵的计算包裹在useMemo的Hook中来缓存。
注意:useMemo 不会让第一次渲染更快。它只会帮助您跳过不必要的更新工作。
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Good:除非 todos 或 filter 改变了,否则不会重新执行这个方法
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
三、当props改变时重置所有state
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Avoid: prop 改变时用useEffect来重置state
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ Good: comment包括其他的state在key改变时会自动的重置
const [comment, setComment] = useState('');
// ...
}
四、当props改变时重置部分state
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Avoid: prop 改变时用useEffect来重置state
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🟢 Better: 渲染改变时调整state
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
为什么要items !== prevItems,不写会怎么样? 这种写法优势在哪里,好在哪里? ...
它必须在prevCount !== count之类的条件内,并且条件内必须有 setPrevCount(count)之类的调用。否则,您的组件重复循环渲染,直到它崩溃。此外,你只能像这样更新当前渲染组件的状态。在渲染过程中调用另一个组件的set函数是错误的。最后,你的 set 调用应该仍然更新状态而不是整体替换改变[3]。 当您在渲染期间调用 set 函数时,React 将在您的组件以返回语句退出后立即重新渲染该组件,然后再渲染子组件。这样,子组件就不需要渲染两次。组件函数的其余部分仍将执行(结果将被丢弃)。
能否重置所有状态
在渲染中计算结果
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: 在渲染期间计算一切
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
这种写法更好,因为它的结果是计算出来的,每次渲染都会重新计算一次结果。对大多数项目遇到这种情况来说,这个显然是更优解。
五、在事件处理程序之间共享逻辑
function ProductPage({ product, addToCart }) {
// 🔴 Avoid: 将特定事件逻辑放在Effect中
useEffect(() => {
if (product.isInCart) {
// 展示通知
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
function ProductPage({ product, addToCart }) {
// ✅ Good: 特定事件逻辑只有在事件被处理时才触发
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
六、POST请求
useEffect(() => {
logVisit(url); // Sends a POST request
}, [url]);
在dev环境中,logVisit 将针对每个 url 调用两次,因此你可能想尝试修复它。 我们建议保持此代码不变。 与前面的示例一样,运行一次和运行两次之间没有用户可见的行为差异。从实际的角度来看,logVisit 不应在开发中执行任何操作,因为你不希望来自开发机器的日志影响生产指标。每次你保存其文件时,你的组件都会remounts,因此无论如何它都会记录开发中的额外访问。
在production环境中,不会访问两次。
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic should run because the component was displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 Avoid: Event-specific logic inside an Effect
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}
第一个请求是合理的,因为触发请求的原因就是表单已经显示渲染,需要获取一次(在dev会触发两次)
第二个不合理,第二个注册并不是由表单渲染而引起的请求,只是在某个特定条件下(比如:用户按下按钮发起请求),此时应该放在特定的事件请求方法中
function handleSubmit(e) {
e.preventDefault();
// ✅ Good: Event-specific logic is in the event handler
post('/api/register', { firstName, lastName });
}
七、计算链
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
}
低效:组件(及其子级)必须在链中的每个 set 调用之间重新渲染。最坏的情况下渲染是这样的:setCard→ 渲染 → setGoldCardCount → 渲染 → setRound → 渲染 → setIsGameOver → 渲染
迭代能力和维护性差:假设后续有新需求,需要时空回溯,获取游戏的每个动作历史。你会通过将每个状态变量更新为过去的一个值来实现。然而,每次修改card就会触发一次Effect链并且更改你需要显示的数据,这种代码往往是僵硬且脆弱的。
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ Calculate what you can during rendering
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ Calculate all the next state in the event handler
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
}
这样效率更高。此外,如果打算做一个查看游戏历史的方法,现在能够将每个状态变量设置为过去的值,而不会触发调整每个其他值的Effect链。如果您需要在多个事件处理程序之间重用逻辑,您可以提取一个函数并从这些处理程序中调用它。
八、初始化应用程序
function App() {
// 🔴 Avoid: Effects with logic that should only ever run once
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
if (typeof window !== 'undefined') { // Check if we're running in the browser.
// ✅ Only runs once per app load
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
九、通知父级状态变更
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
// 🔴 Avoid: The onChange handler runs too late
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])
function handleClick() {
setIsOn(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}
// ...
}
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}
// ...
}
十、将数据传递给父级
function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}
function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 Avoid: Passing data to the parent in an Effect
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}
function Parent() {
const data = useSomeAPI();
// ...
// ✅ Good: Passing data down to the child
return <Child data={data} />;
}
function Child({ data }) {
// ...
}
十一、订阅外部存储
function useOnlineStatus() {
// Not ideal: Manual store subscription in an Effect
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
// ✅ Good: Subscribing to an external store with a built-in Hook
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
十二、获取数据
事先说明,并不是所有获取数据都不可以直接在useEffect中,请看下文分析,部分场景是不适合直接在useEffect中...
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 🔴 Avoid:没有清除逻辑的获取数据
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
缓存响应结果(使用户点击后退按钮时可以立即看到先前的屏幕内容)
如何在服务端获取数据(使服务端初始渲染的 HTML 中包含获取到的内容而不是加载动画)
以及如何避免网络瀑布(使子组件不必等待每个父组件的数据获取完毕后才开始获取数据)
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}
function SearchResults({ query }) {
const [page, setPage] = useState(1);
const params = new URLSearchParams({ query, page });
const results = useData(`/api/search?${params}`);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
那我应该在什么时候用Effect?
回到开始
<Button onClick={(e)=>{query(e)}}>点击发起请求</Button>
总结
如果你可以在渲染期间计算某些内容,则不需要使用 Effect。
想要缓存昂贵的计算,请使用 useMemo 而不是 useEffect。
想要重置整个组件树的 state,请传入不同的 key。
想要在 prop 变化时重置某些特定的 state,请在渲染期间处理。
组件 显示 时就需要执行的代码应该放在 Effect 中,否则应该放在事件处理函数中。
如果你需要更新多个组件的 state,最好在单个事件处理函数中处理。
当你尝试在不同组件中同步 state 变量时,请考虑状态提升。
你可以使用 Effect 获取数据,但你需要实现清除逻辑以避免竞态条件。
参考:
[1] https://react.dev/learn/thinking-in-react#step-3-find-the-minimal-but-complete-representation-of-ui-state
[2] https://react.dev/reference/react/useState#storing-information-from-previous-renders
[3] https://react.dev/reference/react/useState#updating-objects-and-arrays-in-state
[4] https://react.dev/learn/state-as-a-snapshot
[5] https://react.dev/learn/queueing-a-series-of-state-updates
[6] https://react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
微信扫码关注该文公众号作者