Effect是使用来使组件与外部系统同步,比如非React组件、网络或浏览器dom。如不涉及外部系统,比如,你想在某个状态改变时候更新另一个状态,则不需要Effect
移除不需要的Effect
不要使用Effect来转换数据来进行渲染。 例如,你想在列表显示之前进行过滤。可能会写一个Effect进行监听list,然后setFilteredList。这是不必要的,可以直接在组件顶层执行filter来过滤数据,这样组件在每次渲染时候都会进行过滤列表。如果过滤逻辑很复杂,则使用useMemo
不要使用Effect处理用户事件。 例如,点击一个按钮会将物品加入列表,不要监听列表的变化去发送请求,而是在按钮点击函数内进行处理事件发送请求。
示例
当数据依赖于props或者state
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
可以在组件渲染期间生成fullName
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
缓存昂贵的计算
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 Avoid: redundant state and unnecessary Effect
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
可以在渲染期间进行过滤
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ This is fine if getFilteredTodos() is not slow.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
如果计算消耗过多性能,可以使用useMemo监听数据变化,进行复杂计算
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Does not re-run unless todos or filter change
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
如何判断合适需要使用useMemo
使用 console.time ``console.timeEnd
进行两种方式的代码运行时间计算。
重置所有的state当一个props变化时候
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Avoid: Resetting state on prop change in an Effect
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
使用userId作为组件的key,当userId变化时候,key更新,组件会被重置。
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ This and any other state below will reset on key change automatically
const [comment, setComment] = useState('');
// ...
}
调整一些状态当props变化
当items发生变化时候,你要更新当前组件选中的选项为null
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Avoid: Adjusting state on prop change in an Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
上述方式,在items变化时候,React会先使用之前的selection进行渲染,然后更新dom并运行Effect,最后执行setSelection(null)将选中的值置为null,这样组件会进行一次不必要的重新渲染。
相反,应在渲染期间直接调整状态
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Better: Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
当在渲染期间更新组件状态时候,react会丢弃jsx并执行重新渲染。React 还没有渲染 List children 或更新 DOM,所以这让 List children 跳过渲染陈旧的选择值。注意写判断条件避免react进入重新渲染的死循环。
当然,无论怎么做,基于props进行调整当前组件的状态会让数据流难以管理。你可以直接存储选中的id,而非选中的选项。
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: Calculate everything during rendering
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
存储组件选中的id作为组件内部的状态,这样组件的状态不需要依赖于props。渲染的内容可以直接通过id进行筛选。
在事件处理之间共享逻辑
假设您有一个带有两个按钮(购买和结帐)的产品页面,这两个按钮都可以让您购买该产品。 您希望在用户将产品放入购物车时显示通知。 在两个按钮的点击处理程序中调用 showNotification() 感觉很重复,因此您可能想将此逻辑放在 Effect 中:
function ProductPage({ product, addToCart }) {
// 🔴 Avoid: Event-specific logic inside an Effect
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
如果页面会记录当前的购物车的商品,那么每次页面刷新时候也会触发showNotification,这是bug。
当不确定一个事件应该放在effect中还是事件处理函数中的时候。要判断一下这个代码为什么要执行,Effect仅仅使用在当组件已经渲染完成。
function ProductPage({ product, addToCart }) {
// ✅ Good: Event-specific logic is called from event handlers
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
监听状态变化发送请求
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 });
}
// ...
}
发送请求的原因不是来自现在表单渲染的数据,只是想在特定的时间发送请求,这个时刻是用户点击提交按钮,那么发送请求应该放在按钮点击的时间处理函数中
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic runs because the component was displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
function handleSubmit(e) {
e.preventDefault();
// ✅ Good: Event-specific logic is in the event handler
post('/api/register', { firstName, lastName });
}
// ...
}
当选择将一些逻辑放在事件处理函数还是Effect中时候。要从用户的角度判断它是什么逻辑。如果逻辑是由用户的操作引起的,那么逻辑应该放在事件处理函数中,如果逻辑是用户在屏幕上看到的组件引起的,那么使用Effect
计算链
有时您可能会想链接 Effects,每个 Effects 都根据其他状态调整一个状态:
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);
}
}
// ...
多次使用Effect会使组件的逻辑越来越复杂,而且会使代码运行速度变慢。上述代码还有不必要的state引起组件不必要的重新渲染。
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!');
}
}
}
}
// ...
在组件的渲染期间进行判断计算,在事件处理函数中取下一次state进行计算。
初始化
function App() {
// 🔴 Avoid: Effects with logic that should only ever run once
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}
在生产环境中是没问题的,但是在开发环境中,useEffect中的代码会执行两次,可能会导致错误,而且代码在不同环境中的效果应该是相同的。
可以添加一个顶级变量(组件外变量)来判断组件是否运行过。
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
当组件被引入的时候,顶层代码将会执行一次,即使组件没有渲染。
通知父组件组件状态变化
假设您正在编写一个带有内部 isOn 状态的 Toggle 组件,该状态可以是 true 或 false。 有几种不同的方式来切换它(通过单击或拖动)。 您希望在 Toggle 内部状态发生变化时通知父组件,因此您公开一个 onChange 事件并从 Effect 中调用它:
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);
}
}
// ...
}
上述方式过程:点击或者拖动结束,执行setIsOn
,组件进行重新渲染,组件重新渲染完成之后执行useEffect
,通知父组件更新状态,开始另一个渲染过程。
不应该使用Effect进行状态同步,应在事件处理中一次完成两个组件的状态更新。
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
// ✅ Good: Perform all updates during the event that caused them
setIsOn(nextIsOn);
onChange(nextIsOn);
}
function handleClick() {
updateToggle(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}
// ...
}
更进一步,从父组件接收isOn
// ✅ Also good: the component is fully controlled by its parent
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]);
// ...
}
在 React 中,数据从父组件流向它们的子组件。 当您在屏幕上看到错误时,您可以沿着组件链向上追踪信息的来源,直到您找到哪个组件传递了错误的 prop 或具有错误的状态。 当子组件在 Effects 中更新其父组件的状态时,数据流变得很难追踪。 由于子组件和父组件都需要相同的数据,因此让父组件获取该数据,然后将其传递给子组件:
function Parent() {
const data = useSomeAPI();
// ...
// ✅ Good: Passing data down to the child
return <Child data={data} />;
}
function Child({ data }) {
// ...
}
订阅外部数据
有时候,组件可能需要订阅状态,这些状态不是来自React组件,他们可能来自第三方或者来自浏览器。它们会在React不知道的情况下更新,因此需要订阅他们
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();
// ...
}
React专门构建的有hooks来订阅外部数据。
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();
// ...
}
- 如果您可以在渲染期间计算某些东西,则不需要 Effect。
- 要缓存昂贵的计算,请添加 useMemo 而不是 useEffect。
- 要重置整个组件树的状态,请将不同的key传递给它。
- 要重置特定位的状态以响应道具更改,请在渲染期间设置它。
- 因为组件渲染而运行的代码应该在 Effects 中,其余的应该在事件中。
- 如果您需要更新多个组件的状态,最好在单个事件期间执行。
- 每当您尝试同步不同组件中的状态变量时,请考虑提升状态。
- 您可以使用 Effects 获取数据,但您需要实施清理以避免竞争条件。
** 注:文章内容完全来自react官网