react-effects
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Effects: When You Do and Don't Need Them
React Effects:何时需要与何时不需要
Effects are an escape hatch to synchronize React components with external systems (browser APIs, network, third-party libraries). Most component logic does not need Effects. Before writing or keeping a , run through the scenarios below — there's likely a simpler, more performant alternative.
useEffectEffects是React组件与外部系统(浏览器API、网络、第三方库)同步的一个逃生舱口。大多数组件逻辑并不需要Effects。在编写或保留之前,请对照以下场景检查——通常存在更简单、性能更优的替代方案。
useEffectThe Two Questions
两个核心问题
Before every , ask:
useEffect- Is this transforming data for rendering? If yes, compute it during render instead.
- Is this handling a user event? If yes, put it in an event handler instead.
If neither applies, you might actually need an Effect.
在编写每个之前,请先问自己:
useEffect- 这是为渲染转换数据吗? 如果是,在渲染阶段直接计算即可。
- 这是处理用户事件吗? 如果是,将逻辑放在事件处理函数中。
如果两者都不适用,那你可能确实需要一个Effect。
Scenarios Where Effects Are Wrong
Effects被误用的场景
1. Derived State from Props or State
1. 从Props或State派生状态
The most common mistake. If a value can be calculated from existing props or state, it's not state at all — it's a render-time computation.
Why the Effect is harmful: React renders once with stale values, commits to DOM, then the Effect fires a second setState triggering another full render cycle. The user briefly sees outdated UI.
tsx
// WRONG: Redundant state + unnecessary Effect
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// RIGHT: Compute during render — zero extra renders, zero extra state
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;Detection pattern: whose only job is calling .
useEffectsetSomeState(f(props, state))这是最常见的错误。如果一个值可以从现有的props或state计算得出,那它根本不是状态——而是一个渲染时计算值。
Effect的危害: React会先使用过期值渲染一次,提交到DOM后,Effect再触发setState,引发第二次完整渲染周期。用户会短暂看到过时的UI。
tsx
// 错误写法:冗余状态 + 不必要的Effect
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// 正确写法:渲染时计算——无额外渲染,无冗余状态
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;检测模式: 的唯一工作是调用。
useEffectsetSomeState(f(props, state))2. Caching Expensive Computations
2. 缓存昂贵的计算结果
When the computation is genuinely expensive (>1ms in production profiling), use — not an Effect with state.
useMemotsx
// WRONG: Effect + state for caching
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// RIGHT (simple case): Just compute it
const visibleTodos = getFilteredTodos(todos, filter);
// RIGHT (expensive): useMemo skips recomputation when deps haven't changed
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);When is it expensive? Use / in production mode. If the logged time is consistently >=1ms, memoize. Dev mode timings are unreliable due to extra checks.
console.timeconsole.timeEnd当计算确实非常耗时(生产环境分析中耗时>1ms),请使用——而不是带状态的Effect。
useMemotsx
// 错误写法:用Effect + 状态实现缓存
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// 正确写法(简单场景):直接计算
const visibleTodos = getFilteredTodos(todos, filter);
// 正确写法(耗时场景):useMemo会在依赖未变化时跳过重新计算
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);如何判断是否耗时? 在生产模式下使用/。如果记录的时间持续≥1ms,就需要记忆化。开发模式的计时不可靠,因为存在额外的检查逻辑。
console.timeconsole.timeEnd3. Resetting All State When a Prop Changes
3. 当Props变化时重置所有状态
When a prop like changes and you want to clear all component state (form fields, scroll position, etc.), don't reset each piece of state in an Effect — use React's mechanism.
userIdkeyWhy the Effect is harmful: The component renders once with stale state (old comment shown for new user), then the Effect clears it, causing a second render. Every nested component with state needs its own reset Effect — fragile and error-prone.
tsx
// WRONG: Effect to reset state on prop change
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
return /* ... */;
}
// RIGHT: key tells React "this is a different component instance"
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState(''); // Auto-resets when key changes
return /* ... */;
}Detection pattern: resetting multiple states.
useEffect(() => { setX(initial); setY(initial); ... }, [someProp])当这类Props变化,你需要清空组件的所有状态(表单字段、滚动位置等)时,不要在Effect中逐个重置状态——使用React的机制。
userIdkeyEffect的危害: 组件会先使用过期状态渲染一次(为新用户显示旧评论),然后Effect清空状态,引发第二次渲染。每个带状态的嵌套组件都需要自己的重置Effect——脆弱且容易出错。
tsx
// 错误写法:用Effect在Props变化时重置状态
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
return /* ... */;
}
// 正确写法:key告诉React“这是一个不同的组件实例”
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState(''); // 当key变化时自动重置
return /* ... */;
}检测模式: 用于重置多个状态。
useEffect(() => { setX(initial); setY(initial); ... }, [someProp])4. Adjusting Some State When a Prop Changes
4. 当Props变化时调整部分状态
Sometimes you don't want to reset all state — just adjust one piece. The best approach is often to avoid the state entirely and derive the value.
tsx
// WRONG: Effect to clear selection when items change
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
return /* ... */;
}
// BETTER: Store the ID, derive the selected object
function List({ items }) {
const [selectedId, setSelectedId] = useState(null);
// If the selected item is still in the list, keep it; otherwise null
const selection = items.find(item => item.id === selectedId) ?? null;
return /* ... */;
}If you truly must adjust state during render (rare), you can do so without an Effect, but this pattern should be a last resort:
tsx
function List({ items }) {
const [prevItems, setPrevItems] = useState(items);
const [selection, setSelection] = useState(null);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
}有时你不想重置所有状态——只是调整其中一个值。最佳方法通常是完全避免该状态,而是派生这个值。
tsx
// 错误写法:用Effect在列表变化时清空选中项
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
return /* ... */;
}
// 更优写法:存储ID,派生选中的对象
function List({ items }) {
const [selectedId, setSelectedId] = useState(null);
// 如果选中的项仍在列表中则保留,否则设为null
const selection = items.find(item => item.id === selectedId) ?? null;
return /* ... */;
}如果你确实必须在渲染时调整状态(这种情况应该是最后手段),可以不使用Effect:
tsx
function List({ items }) {
const [prevItems, setPrevItems] = useState(items);
const [selection, setSelection] = useState(null);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
}5. Event-Specific Logic in Effects
5. Effect中包含事件专属逻辑
If code should run because the user did something (clicked a button, submitted a form), it belongs in an event handler — not an Effect that reacts to state changes.
Why the Effect is harmful: The logic runs whenever the tracked state changes, including on page load, navigation, or other state restorations — not just in response to the user action.
tsx
// WRONG: Shows notification whenever product.isInCart becomes true
// (including page refresh, back navigation, etc.)
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
}
// RIGHT: Notification is a direct response to user action
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
}Detection pattern: that runs , , , or other side effects triggered by that was set in an event handler.
useEffectshowNotificationnavigatealert[someFlag]如果代码应该因为用户操作(点击按钮、提交表单)而运行,那它应该放在事件处理函数中——而不是响应状态变化的Effect中。
Effect的危害: 只要跟踪的状态变化,逻辑就会运行,包括页面加载、导航或其他状态恢复时——而不仅仅是响应用户操作。
tsx
// 错误写法:只要product.isInCart变为true就显示通知
//(包括页面刷新、返回导航等场景)
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
}
// 正确写法:通知是对用户操作的直接响应
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
}检测模式: 中运行、、或其他副作用,且由事件处理函数中设置的触发。
useEffectshowNotificationnavigatealert[someFlag]6. POST Requests Triggered by User Actions
6. 用户操作触发的POST请求
Sending data to a server in response to a user action (form submit, button click) belongs in the event handler. Only truly display-driven requests (like analytics page views) belong in Effects.
tsx
// WRONG: Roundabout way to send form data
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// RIGHT: Submit directly in the event handler
function handleSubmit(e) {
e.preventDefault();
post('/api/register', { firstName, lastName });
}
// This analytics Effect IS correct — it runs because the component displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);响应用户操作(表单提交、按钮点击)向服务器发送数据的逻辑应该放在事件处理函数中。只有真正由显示驱动的请求(比如分析页面浏览量)才应该放在Effects中。
tsx
// 错误写法:迂回的表单数据提交方式
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// 正确写法:直接在事件处理函数中提交
function handleSubmit(e) {
e.preventDefault();
post('/api/register', { firstName, lastName });
}
// 这个分析Effect是正确的——它因组件显示而运行
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);7. Chains of Effects
7. Effect链式调用
Multiple Effects where each one sets state that triggers the next Effect. This creates a cascade of unnecessary renders and makes the logic hard to follow.
Why the Effect chain is harmful: Each setState in the chain triggers a separate render pass. If there are 4 Effects in the chain, the component renders 5 times instead of once. The logic is scattered across multiple Effects making it hard to trace.
tsx
// WRONG: Chain of Effects triggering 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]);
// RIGHT: Derive what you can, compute the rest in the event handler
const isGameOver = round > 5; // Derived, not state
function handlePlaceCard(nextCard) {
if (isGameOver) throw Error('Game already ended.');
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}Detection pattern: Multiple hooks where one sets state that appears in another's dependency array.
useEffect多个Effect互相触发,每个Effect设置的状态会触发下一个Effect。这会创建不必要的渲染级联,且逻辑难以追踪。
Effect链式调用的危害: 链式中的每个setState都会触发单独的渲染过程。如果链式中有4个Effects,组件会渲染5次而不是1次。逻辑分散在多个Effect中,难以追踪。
tsx
// 错误写法:互相触发的Effect链式调用
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]);
// 正确写法:尽可能派生值,其余逻辑放在事件处理函数中
const isGameOver = round > 5; // 派生值,而非状态
function handlePlaceCard(nextCard) {
if (isGameOver) throw Error('Game already ended.');
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}检测模式: 多个钩子,其中一个设置的状态出现在另一个的依赖数组中。
useEffect8. Application Initialization
8. 应用初始化
Code that should run once per app load (not once per component mount), like checking auth tokens or loading config from localStorage.
tsx
// WRONG: Runs twice in StrictMode development, may cause issues
function App() {
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
}
// RIGHT (option A): Module-level guard
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
}
// RIGHT (option B): Module-level execution (runs on import, once)
if (typeof window !== 'undefined') {
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}应该在应用加载时运行一次(而非组件挂载时运行一次)的代码,比如检查认证令牌或从localStorage加载配置。
tsx
// 错误写法:在StrictMode开发模式下会运行两次,可能引发问题
function App() {
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
}
// 正确写法(选项A):模块级守卫
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
}
// 正确写法(选项B):模块级执行(导入时运行一次)
if (typeof window !== 'undefined') {
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}9. Notifying Parent Components of State Changes
9. 通知父组件状态变化
Using an Effect to call a parent's callback after local state changes creates an extra render cycle and makes the update order unpredictable.
tsx
// WRONG: Effect to notify parent — renders twice
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
function handleClick() {
setIsOn(!isOn);
}
}
// RIGHT: Update child + notify parent in the same event
// React batches both setState calls into a single render
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn); // Parent updates in the same batch
}
function handleClick() {
updateToggle(!isOn);
}
}
// BEST: Fully controlled — no local state at all
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
}Detection pattern: .
useEffect(() => { onSomething(localState); }, [localState, onSomething])使用Effect在本地状态变化后调用父组件的回调函数会创建额外的渲染周期,且更新顺序不可预测。
tsx
// 错误写法:用Effect通知父组件——会渲染两次
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
function handleClick() {
setIsOn(!isOn);
}
}
// 正确写法:在同一事件中更新子组件+通知父组件
// React会将两次setState调用批处理为一次渲染
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn); // 父组件在同一批处理中更新
}
function handleClick() {
updateToggle(!isOn);
}
}
// 最优写法:完全受控——无本地状态
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
}检测模式: 。
useEffect(() => { onSomething(localState); }, [localState, onSomething])10. Passing Data Up to Parent
10. 向父组件传递数据
Child fetches data, then uses an Effect to push it up to the parent. This inverts React's data flow and makes bugs hard to trace.
tsx
// WRONG: Child fetches, then pushes data to parent via Effect
function Parent() {
const [data, setData] = useState(null);
return <Child onFetched={setData} />;
}
function Child({ onFetched }) {
const data = useSomeAPI();
useEffect(() => {
if (data) onFetched(data);
}, [onFetched, data]);
}
// RIGHT: Parent owns the data fetching, passes data down
function Parent() {
const data = useSomeAPI();
return <Child data={data} />;
}Principle: Data flows down in React. If a child needs data and the parent also needs it, the parent should fetch it and pass it down.
子组件获取数据,然后使用Effect将数据推送给父组件。这会反转React的数据流,且难以追踪bug。
tsx
// 错误写法:子组件获取数据,然后通过Effect推送给父组件
function Parent() {
const [data, setData] = useState(null);
return <Child onFetched={setData} />;
}
function Child({ onFetched }) {
const data = useSomeAPI();
useEffect(() => {
if (data) onFetched(data);
}, [onFetched, data]);
}
// 正确写法:父组件负责数据获取,将数据向下传递
function Parent() {
const data = useSomeAPI();
return <Child data={data} />;
}原则: React中数据向下流动。如果子组件和父组件都需要某数据,应该由父组件获取并向下传递。
11. Subscribing to External Stores
11. 订阅外部存储
Subscribing to browser APIs or external data sources that change outside React's control (e.g., , browser history, third-party state libraries).
navigator.onLinetsx
// SUBOPTIMAL: Manual subscription in Effect
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function update() { setIsOnline(navigator.onLine); }
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return isOnline;
}
// RIGHT: useSyncExternalStore — purpose-built for this
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // Client snapshot
() => true // Server snapshot
);
}订阅浏览器API或不受React控制的外部数据源(如、浏览器历史、第三方状态库)。
navigator.onLinetsx
// 次优写法:在Effect中手动订阅
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function update() { setIsOnline(navigator.onLine); }
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return isOnline;
}
// 正确写法:useSyncExternalStore——专为该场景设计
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // 客户端快照
() => true // 服务端快照
);
}12. Data Fetching
12. 数据获取
This is one case where an Effect is appropriate — you need to synchronize with the network. But you must handle race conditions with a cleanup flag.
tsx
// CORRECT: Effect with ignore flag for race condition handling
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => { ignore = true; };
}, [query, page]);For production apps, prefer extracting data fetching into a custom hook or using a library like TanStack Query / SWR that handles caching, deduplication, and race conditions automatically.
这是Effect适用的场景之一——你需要与网络同步。但必须使用清理标志处理竞态条件。
tsx
// 正确写法:带ignore标志处理竞态条件的Effect
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => { ignore = true; };
}, [query, page]);对于生产应用,建议将数据提取逻辑封装到自定义钩子中,或使用TanStack Query / SWR这类库,它们会自动处理缓存、去重和竞态条件。
Quick Reference: Do I Need This Effect?
快速参考:我需要这个Effect吗?
| What the Effect does | Alternative |
|---|---|
| Computes a value from props/state | Compute during render (or |
| Resets all state when a prop changes | Add a |
| Adjusts some state when a prop changes | Derive the value instead of storing it |
| Runs code when user clicks/submits | Move to event handler |
| Sends POST request from user action | Move to event handler |
| Sets state that triggers another Effect | Consolidate into one event handler |
| Initializes app once | Module-level code or |
Calls parent's | Call |
| Pushes data from child to parent | Lift data fetching to parent |
| Subscribes to external data source | Use |
| Fetches data | Keep the Effect but add cleanup; prefer TanStack Query |
| Effect的作用 | 替代方案 |
|---|---|
| 从props/state计算值 | 渲染时计算(或在耗时场景使用 |
| 在Props变化时重置所有状态 | 添加 |
| 在Props变化时调整部分状态 | 派生值而非存储状态 |
| 用户点击/提交时运行代码 | 移至事件处理函数 |
| 用户操作触发POST请求 | 移至事件处理函数 |
| 设置状态以触发另一个Effect | 合并到单个事件处理函数 |
| 初始化应用一次 | 模块级代码或 |
本地setState后调用父组件 | 在同一事件处理函数中调用 |
| 子组件向父组件推送数据 | 将数据获取逻辑提升到父组件 |
| 订阅外部数据源 | 使用 |
| 获取数据 | 保留Effect并添加清理逻辑;优先使用TanStack Query |
Review Checklist
评审检查清单
When reviewing or writing a , verify:
useEffect- Necessity: Can this be a render-time computation, , or event handler instead?
useMemo - Cleanup: Does the Effect clean up subscriptions, timers, or connections?
- Race conditions: Does async work use an flag or abort controller?
ignore - Dependencies: Are all reactive values listed? No for exhaustive-deps?
eslint-disable - No chains: Does setting state in this Effect trigger another Effect? If so, consolidate.
- Not a lifecycle: Is this genuinely about synchronizing with an external system, or is it disguised /
componentDidMountthinking?componentDidUpdate
在评审或编写时,请验证:
useEffect- 必要性: 能否用渲染时计算、或事件处理函数替代?
useMemo - 清理逻辑: Effect是否清理了订阅、计时器或连接?
- 竞态条件: 异步操作是否使用了标志或中止控制器?
ignore - 依赖项: 是否列出了所有响应式值?是否禁用了exhaustive-deps的ESLint检查?
- 无链式调用: 该Effect中设置的状态是否会触发另一个Effect?如果是,合并逻辑。
- 不是生命周期: 这真的是与外部系统同步的逻辑,还是伪装成/
componentDidMount的生命周期思维?componentDidUpdate