从Effects拆分出Events

当操作交互时触发事件处理程序执行。不像事件处理程序,Effects是响应式的,当依赖列表中的依赖(如prop或state变量)发生改变时,Effects响应依赖的改变同步执行Effects。

有时候,你可能希望这两种方式一起混合使用:Effect响应一些值重新运行,而对另外一些值不做响应。这篇文章介绍如何混合使用这两种方式。

事件处理函数和Effects的选择

首先,我们看看时间处理函数和Effects的区别。

假如你正在实现一个聊天室组件,包含有以下2个功能:

  1. 根据下拉框选择的聊天室自动建立连接
  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组件接收一个urlprop,这个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使用了urlprop,所以在每一次重新渲染后且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旁边。

参考文献:

Separating Events from Effects

相关推荐
PleaSure乐事8 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
getaxiosluo8 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
新星_9 小时前
函数组件 hook--useContext
react.js
阿伟来咯~11 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端11 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱11 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
bysking12 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
September_ning16 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人16 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱00116 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js