当操作交互时触发事件处理程序执行。不像事件处理程序,Effects是响应式的,当依赖列表中的依赖(如prop或state变量)发生改变时,Effects响应依赖的改变同步执行Effects。
有时候,你可能希望这两种方式一起混合使用:Effect响应一些值重新运行,而对另外一些值不做响应。这篇文章介绍如何混合使用这两种方式。
事件处理函数和Effects的选择
首先,我们看看时间处理函数和Effects的区别。
假如你正在实现一个聊天室组件,包含有以下2个功能:
- 根据下拉框选择的聊天室自动建立连接
- 点击"发送"按钮发送聊天信息
事件处理程序响应指定的交互
对用户来说,当用户点击发送按钮时,应该发送消息。如果你在其他时间或因为其他原因发送消息用户会不安。因此,发送消息应该使用事件处理函数:
javascript
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
使用事件处理函数,你可以确保仅仅用户点击发送按钮时sendMessage(message)
。
需要随时同步时使用Effect
和聊天室建立连接不需要关注界面是怎么操作的,但是建立的连接需要和界面下拉选择的聊天室同步。甚至在初次渲染的时候,用户没有做任何界面操作,仍然需要建立聊天室的连接。所以,建立聊天室连接是一个Effect:
javascript
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
不论用户怎么样的界面操作,当roomId改变后,Effect实时同步。
响应式逻辑
事件处理程序需要手动触发,如点击按钮触发。Efefct是自动触发的,当依赖改变时同步Effect。
- 事件处理程序的逻辑不是响应式的。 除非用户操作某个特定的交互,事件处理程序才会执行。
- Effects的逻辑是响应式的。 如果重新渲染使依赖的值发生改变,则Effect响应实时同步。
从Effects中抽离出非响应式逻辑
当你把响应式逻辑和非响应式逻辑混合在一起时,事情更棘手。
假如你想在成功建立连接后,在界面展示一个通知。同时你从props获取显示通知的样式主题theme。
javascript
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...
theme是一个响应式的值,所以应该把theme放到依赖列表中:
javascript
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ All dependencies declared
// ...
当roomId改变时,重新建立连接是没有问题的。但是由于theme也是一个依赖,当深色主题和浅色主题切换改变时也会重新建立连接,这不是我们想要的。
也就是说,你不想让下面的代码是响应式的,即使它在Effect中并且是响应式的值:
javascript
// ...
showNotification('Connected!', theme);
// ...
你需要找出一种方式从响应式Effect中抽离出非响应式的逻辑。
声明Effect Event
使用一个特殊的HookuseEffectEvent
把非响应逻辑从Effect抽离出来。
javascript
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...
在这里onConnected叫做Effect Event。它是Effect逻辑的一部分,但是它的行为很像一个事件处理函数。它的逻辑不是响应式的,它读取的是props和state的最新值。
现在我们看看如何在Effect中调用onConnected:
javascript
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
这样就解决了问题。注意onConnected从依赖列表中移除。Effect Events不是响应式的,不能放到依赖列表中。
Effect Events和事件处理程序很相似的。主要的区别是时间处理程序响应用户的交互,Effect Events在Effects中由你触发。
使用Effect Event读props和state的最新值
例如,假设有一个记录页面访问的Effect:
javascript
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}
然后,你向你的网站添加多个路由。现在Page组件接收一个url
prop,这个url表示当前的url路径。代码如下:
javascript
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
现在,你想把购物车加入的商品的数量一起发送:
javascript
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}
在Effect你使用了numberOfItems
,linter要求你把它加入依赖列表中。但是当numberOfItems
改变时,你不想调用logVisit
记录一次访问。当用户把商品放到购物车时,即numberOfItems
改变时,并不是说用户又一次访问了页面。
下面把代码分成两部分:
javascript
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
在这里onVisit
是一个Effect Event,它不是响应式的。虽然使用了响应式的值numberOfItems
,但是numberOfItems
改变不会执行Effect Event。
Effect本身是响应式的。Effect使用了url
prop,所以在每一次重新渲染后且url改变了,就会重新执行Effect。在Effect里面的调用onVisit
Effect Event。
url改变的时候就会调用logVisit,并且能够读取到numberOfItems
的最新值。
Effect Events的限制
在使用Effect Events时,会有限制:
- 仅仅在Effects内部能调用它
- 不能把它传递给其他的组件或Hools
例如,不能像下面这样传递Effect Events:
javascript
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 Avoid: Passing Effect Events
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Need to specify "callback" in dependencies
}
相反,总是直接在使用它的Effect旁边声明Effect Events:
javascript
function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Good: Only called locally inside an Effect
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // No need to specify "onTick" (an Effect Event) as a dependency
}
Effect Events是Effect代码非响应式的片段,所以应该在使用它的Effect旁边。
参考文献: