好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

React + TypeScript:将逻辑分离为事件和效果

在撰写本文时,“React Docs”(BETA)正在发布。其中一篇文章在 将事件与效果分离 是一篇详细的文章,解释了如何分离事件和效果的逻辑。

虽然这篇文章基本涵盖了文章的信息,但并不是日文翻译。我补充了缺失的部分,改变了解释的方式,删除了我认为不必要的描述。此外,与官网不同的是,示例代码分为模块,还引入了 TypeScript。

事件处理程序仅在执行预定交互时才会重新执行。另一方面,效果是在读取的值(例如属性和状态变量)与上次渲染期间不同时重新同步。在某些情况下,您可能希望将这两个动作结合起来。当您希望效果根据某些值重新运行而不响应其他值时。让我解释一下这个过程是如何工作的。

效果没有任何反应依赖
效果没有任何反应依赖 # 在事件处理程序和效果之间进行选择
首先,回顾一下事件处理程序和效果的不同之处。

假设您正在实现一个聊天室组件。有两个要求:

组件将自动连接到选定的聊天室。 单击发送按钮将消息发送到聊天室。

我想出了要实现的代码。但是应该放在哪里呢?添加事件处理程序或效果。当这个问题出现时,想想为什么你必须运行代码 什么是效果,它与事件有何不同? “参考)。

事件处理程序被执行以响应定义的交互

从用户的角度来看,应该在单击标记为“发送”的按钮时发送消息。如果与其他时间或操作一起发送,用户会感到困惑。换句话说,发送消息应该是一个事件处理程序。事件处理程序处理预先确定的交互,例如点击。

src/ChatRoom.tsx

  export   const   ChatRoom  :   FC  <  Props  >   =   ({   roomId  ,   serverUrl   })   =>   { 
	 const   [  message  ,   setMessage  ]   =   useState  (  ''  ); 
	 const   handleSendClick   =   ()   =>   { 
		 sendMessage  (  message  ); 
		 setMessage  (  ''  ); 
	 }; 
	 // ... 
	 return   ( 
		 <> 
			 {  /* ... */  } 
			 <  input 
				 value  =  {  message  } 
				 onChange  =  {  ({   target  :   {   value   }   })   =>   setMessage  (  value  )  } 
			 /> 
			 <  button   onClick  =  {  handleSendClick  }  > Send </  button  > 
		 </> 
	 ); 
 }; 
 

使用事件处理程序,很明显 sendMessage(message) 仅在用户按下按钮时执行。

需要同步时执行效果

该组件必须在聊天期间保持与房间的连接。我应该在哪里编写代码?

这段代码的执行不是基于固定的交互。用户如何或为何转换到聊天室屏幕并不重要。一旦打开屏幕进行查看和交互,该组件应保持与所选聊天服务器的连接。即使聊天室组件是应用程序的初始屏幕并且用户没有任何交互,仍然应该保持连接。在这种情况下使用效果。

src/ChatRoom.tsx

  export   const   ChatRoom  :   FC  <  Props  >   =   ({   roomId  ,   serverUrl   })   =>   { 
	 // ... 
	 useEffect  (()   =>   { 
		 const   connection   =   createConnection  (  serverUrl  ,   roomId  ); 
		 connection  .  connect  (); 
		 return   ()   =>   connection  .  disconnect  (); 
	 },   [  roomId  ,   serverUrl  ]); 
	 // ... 
 }; 
 

此代码将始终连接到选定的聊天服务器。它不涉及用户交互。也许您刚刚打开了一个应用程序,移动到另一个房间,或者从您转换到的另一个屏幕返回。尽管如此,效果仍然与组件当前选择的房间保持同步,并根据需要重新连接(注意:React + TypeScript:React 18 在组件安装时运行 useEffect 两次)。此代码示例已作为示例 001 发布到 CodeSandbox。

示例 001 React + TypeScript:从效果中分离事件 01

反应式价值观和逻辑

很简单,事件处理程序和效果之间的区别如下。

事件处理程序:“手动”执行。 例如单击按钮。 效果:在“自动”上同步。 无论交互如何,都需要。

在组件主体中声明的属性和状态变量称为“反应性值”。在下面的代码示例中, serverUrl 不再是反应值。 roomId 和 message 是响应式值,参与渲染数据流。

src/ChatRoom.tsx

  const   serverUrl   =   '  https://localhost:1234  '  ; 
 // export const ChatRoom: FC<Props> = ({ roomId, serverUrl }) => { 
 export   const   ChatRoom  :   FC  <  Props  >   =   ({   roomId   })   =>   { 
	 const   [  message  ,   setMessage  ]   =   useState  (  ''  ); 
	 // ... 
 } 
 

反应值可能会在重新渲染时发生变化。例如,用户编辑 message 。您还可以在下拉列表中选择不同的 roomId 。事件处理程序和效果的不同之处在于它们对值更改的响应方式。

写在事件处理程序中的逻辑不是反应式的 .只有当用户重复相同的交互(如点击)时才会重新执行。事件处理程序可以读取反应值。但是,它对价值的变化不是“反应性的”。 写入内部效果的逻辑是反应式的 .效果读取的反应性值必须添加到依赖项中(请参阅使效果对'反应性'值作出反应)。如果在重新渲染时该值发生了变化,React 将使用新值重新运行效果的逻辑。

保持反应值和逻辑分开。有时最好将组件主体中声明的“反应性值”的处理放在“非反应性逻辑”中。让我们再次看一下前面的代码示例。

事件处理程序中的逻辑不是反应式的

请参阅下面的代码。这个逻辑是被动的吗?

  // ... 
 sendMessage  (  message  ); 
 // ... 
 

从用户的角度来看, message 重写不想发送值。它只是意味着用户正在输入。换句话说,发送消息的逻辑不应该是被动的。不要仅仅因为“反应值”发生了变化而重新运行。所以我把这个逻辑放在事件处理程序中。

  const   handleSendClick   =   ()   =>   { 
	 sendMessage  (  message  ); 
	 // ... 
 }; 
 

事件处理程序不是反应式的。因此 sendMessage(message) 中的逻辑仅在用户单击“发送”按钮时运行。

逻辑内部效应是反应性的

考虑以下代码。

  // ... 
 const   connection   =   createConnection  (  serverUrl  ,   roomId  ); 
 connection  .  connect  (); 
 // ... 
 

从用户的角度来看, roomId 的变化是因为 你想连接到另一个房间 是。因此,连接房间的逻辑应该是反应式的。这些代码应该“意识到”“反应性值”,并在值不同时重新执行。所以把这个逻辑放在一个效果里。

  useEffect  (()   =>   { 
	 const   connection   =   createConnection  (  serverUrl  ,   roomId  ); 
	 connection  .  connect  (); 
	 return   ()   =>   connection  .  disconnect  (); 
 },   [  roomId  ,   serverUrl  ]); 
 

效果是反应性的。因此代码 createConnection(serverUrl, roomId) 和 connection.connect() 将针对每个不同的依赖值( [roomId, serverUrl] )执行。该效果会将聊天连接同步到当前选择的房间。

此外, 样品 001 serverUrl 不是响应式的,因为它是在组件 App 之外声明的。但是,它作为属性传递给 ChatRoom 。因此,它是子组件的反应值。

从效果中去除非反应性逻辑

当你混合反应式和非反应式逻辑时,它变得棘手。

例如,假设您想在用户连接到聊天时显示通知。通知背景颜色的当前主题是从属性中读取的,可以是深色或浅色。以正确的颜色显示通知。

  export   const   ChatRoom  :   FC  <  Props  >   =   ({   theme  ,   roomId  ,   serverUrl   })   =>   { 
	 useEffect  (()   =>   { 
		 const   connection   =   createConnection  (  serverUrl  ,   roomId  ); 
		 connection  .  on  (  '  connected  '  ,   ()   =>   { 
			 showNotification  (  '  Connected!  '  ,   theme  ); 
		 }); 
		 connection  .  connect  (); 
		 // ... 
	 },   [  roomId  ,   serverUrl  ]); 

 }; 
 

但是, theme 是一个反应值(可能会随着重新渲染而改变)。然后在依赖声明中包含您的效果读取的任何反应值(请参阅验证 React 是否已将所有反应值包含在您的依赖项中)。因此 theme 必须添加为依赖项。

  export   const   ChatRoom  :   FC  <  Props  >   =   ({   theme  ,   roomId  ,   serverUrl   })   =>   { 
	 useEffect  (()   =>   { 
		 const   connection   =   createConnection  (  serverUrl  ,   roomId  ); 
		 connection  .  on  (  '  connected  '  ,   ()   =>   { 
			 showNotification  (  '  Connected!  '  ,   theme  ); 
		 }); 
		 connection  .  connect  (); 
		 return   ()   =>   connection  .  disconnect  (); 
	 },   [  theme  ,   roomId  ,   serverUrl  ]);   // ✅ すべての依存関係を宣言 
	 // ... 
 }; 
 

您可以在下面的示例 002 中看到此代码的运行情况。尝试一下,看看您在用户体验方面遇到了什么问题。

示例 002 React + TypeScript:从效果中分离事件 02

当 roomId 更改时,聊天会重新连接。此举按预期工作。但是,我还包括 theme 作为依赖项。因此,即使您只是在深色和浅色主题之间切换,每次聊天都会重新连接。这是个问题。

换句话说,即使此代码在效果(反应式逻辑)内,我也不希望它反应式运行。

  // ... 
 showNotification  (  '  Connected!  '  ,   theme  ); 
 // ... 
 

我们需要一种方法来将这种非反应性逻辑与反应性效果分开。

声明一个事件函数(实验 API)

[注意] 本节描述的 API 是实验性的。它还没有在 React 的常规版本中提供。

useEvent 是一个特殊的钩子,可以将非反应性逻辑从效果中提取出来。

  import   {   useEffect  ,   useEvent   }   from   '  react  '  ; 

 export   const   ChatRoom  :   FC  <  Props  >   =   ({   theme  ,   roomId  ,   serverUrl   })   =>   { 
	 const   onConnected   =   useEvent  (()   =>   { 
		 showNotification  (  '  Connected!  '  ,   theme  ); 
	 }); 
	 // ... 
 }; 
 

在这段代码中,我们将 onConnected 称为“事件函数”。甚至效果逻辑的一部分也表现得像一个事件处理程序。事件函数内部的逻辑不能是反应式的。属性和状态总是“看到”它们最近的值。

这就是我们从效果中调用 onConnected 事件函数的方式。

  export   const   ChatRoom  :   FC  <  Props  >   =   ({   theme  ,   roomId  ,   serverUrl   })   =>   { 
	 const   onConnected   =   useEvent  (()   =>   { 
		 showNotification  (  '  Connected!  '  ,   theme  ); 
	 }); 
	 useEffect  (()   =>   { 
		 const   connection   =   createConnection  (  serverUrl  ,   roomId  ); 
		 connection  .  on  (  '  connected  '  ,   ()   =>   { 
			 onConnected  (); 
		 }); 
		 connection  .  connect  (); 
		 return   ()   =>   connection  .  disconnect  (); 
	 },   [  roomId  ,   serverUrl  ]);   // ✅ すべての依存関係を宣言 
	 // ... 
 }; 
 

问题已经解决了。与从 useState 返回的 set 函数一样,事件函数不会浮动。重新渲染时它不会改变。从效果的依赖列表中排除事件函数加载的值。因为它不再是一个反应值。

请参阅下面的示例 003,以查看修改后的代码是否按预期工作。

示例 003 React + TypeScript:从效果中分离事件 03

事件函数与事件处理程序非常相似。主要区别在于事件处理程序是响应用户交互而执行的,而事件函数是从效果中调用的。事件函数在你的效果的反应逻辑和不应该反应的代码之间“打破链条”。

使用事件函数读取最新的属性和状态(实验 API)

[注意] 本节描述的 API 是实验性的。它还没有在 React 的常规版本中提供。

有时你可能想要抑制依赖 linter。其中许多情况可以通过事件函数来修复。

例如,假设您有一个记录页面访问的效果。

  export   const   Page  :   FC   =   ()   =>   { 
	 useEffect  (()   =>   { 
		 logVisit  (); 
	 },   []); 
	 // ... 
 }; 
 

后来,我向该站点添加了多条路线。所以我们给 Page 组件的接收属性是 url 和当前路径。如果您尝试将此 url 作为参数传递给 logVisit 调用,则依赖项 linter 会警告您。

React Hook useEffect 缺少依赖项:'url'

现在你必须考虑你想让你的代码做什么。您需要分别记录对不同 URL 的访问。因为每个 URL 代表一个不同的页面。也就是说,对 logVisit 的调用预计会响应 url 。因此,根据 linter,您应该将 url 添加到您的依赖项中。

  export   const   Page  :   FC  <  Props  >   =   ({   url   })   =>   { 
	 useEffect  (()   =>   { 
		 logVisit  (  url  ); 
		 // }, []); // ? 依存関係にurlが含まれていない 
	 },   [  url  ]);   // ✅ すべての依存関係を宣言 
	 // ... 
 }; 
 

此外,假设您想将购物车中的商品数量添加到页面访问日志中。将 numberOfItems 添加到 logVisit 参数中,依赖linter 也会注意到。

React Hook useEffect 缺少依赖项:'numberOfItems'

  export   const   Page  :   FC  <  Props  >   =   ({   url   })   =>   { 
	 const   {   items   }   =   useContext  (  ShoppingCartContext  ); 
	 const   numberOfItems   =   items  .  length  ; 
	 useEffect  (()   =>   { 
		 // logVisit(url); 
		 logVisit  (  url  ,   numberOfItems  ); 
	 },   [  url  ]);   // ? 依存関係にnumberOfItemsが含まれていない 
	 // ... 
 }; 
 

因为在效果中使用了 numberOfItems ,所以 linter 要求我将值包含在依赖项中。但是,我不希望 logVisit 调用对 numberOfItems 产生反应。当用户将东西放入购物车时, numberOfItems 的值会发生变化。但这并不意味着用户重新访问了该页面。因此,对页面的访问更像是一个事件。你会想知道页面被访问的确切时间。

为此,请使用 useEvent 将逻辑一分为二。

  export   const   Page  :   FC  <  Props  >   =   ({   url   })   =>   { 
	 const   {   items   }   =   useContext  (  ShoppingCartContext  ); 
	 const   numberOfItems   =   items  .  length  ; 
	 const   onVisit   =   useEvent  ((  visitedUrl  :   string  )   =>   { 
		 logVisit  (  visitedUrl  ,   numberOfItems  ); 
	 }); 
	 useEffect  (()   =>   { 
		 onVisit  (  url  ); 
	 },   [  url  ]);   // ✅ すべての依存関係を宣言 
	 // ... 
 }; 
 

这段代码中的 onVisit 是事件函数。函数体中的逻辑不是反应式的。因此,如果您在代码中使用的 numberOfItems (或任何其他反应性值)发生更改,您的效果中的其他代码将不会重新运行。 linter 甚至不会要求您在依赖项中包含 numberOfItems 。

但是,效果仍然是被动的。效果内部的逻辑使用 url 属性,因此每次使用不同的 url 重新渲染时都会重新运行。此时,事件函数 onVisit 也被调用。

因此,每次 url 更改时都会调用 logVisit ,始终读取最新的 numberOfItems 。但是,事件函数逻辑不会因为 numberOfItems 本身发生变化而重新执行。

传递给事件函数的参数

您可能认为 onVisit() 可以不带参数调用并从事件函数中读取 url 。

  const   onVisit   =   useEvent  (()   =>   { 
	 logVisit  (  url  ,   numberOfItems  ); 
 }); 
 useEffect  (()   =>   { 
	 onVisit  (); 
 },   [  url  ]); 
 

即使这样也有效。但是,最好将 url 显式传递给事件函数。 使用 url 作为函数参数表明从用户的角度转换到不同的 url 页面构成了另一个“事件”。 . visitedUrl 是发生的“事件”的一部分。

  const   onVisit   =   useEvent  ((  visitedUrl  :   string  )   =>   { 
	 logVisit  (  visitedUrl  ,   numberOfItems  ); 
 }); 
 useEffect  (()   =>   { 
	 onVisit  (  url  ); 
 },   [  url  ]); 
 

事件函数 ( onVisit ) 明确“要求” visitedUrl 作为参数。因此,您不能再无意中从效果的依赖项中删除 url 。如果您从依赖项中删除 url (因为即使您转换到另一个页面,它也不会识别值更改),linter 将发出警告。 onVisit 应该对 url 具有反应性。因此,我们不是直接从函数中读取 url (使其反应较少),而是通过效果传递值。

如果您的效果包含异步逻辑,这一点尤其重要。

  const   onVisit   =   useEvent  ((  visitedUrl  :   string  )   =>   { 
	 logVisit  (  visitedUrl  ,   numberOfItems  ); 
 }); 
 useEffect  (()   =>   { 
	 setTimeout  (()   =>   { 
		 onVisit  (  url  ); 
	 },   5000  );   // 訪問のログを遅らせる 
 },   [  url  ]); 
 

在此代码示例中,从事件函数 onVisit 中引用的 url 对应于最新的属性值(可能已经更改)。但是,当执行效果(以及 onVisit 调用的异步处理)时,参数中传递的 visitedUrl 将是 url 的值。

限制依赖linter的规则可以吗?

在现有代码库中,您可能会发现 lint 规则受到如下限制:

  export   const   Page  :   FC  <  Props  >   =   ({   url   })   =>   { 
	 const   {   items   }   =   useContext  (  ShoppingCartContext  ); 
	 const   numberOfItems   =   items  .  length  ; 
	 // ? つぎのようなリンターの制限は避ける 
	 // eslint-disable-next-line react-hooks/exhaustive-deps 
	 useEffect  (()   =>   { 
		 logVisit  (  url  ,   numberOfItems  ); 
	 },   [  url  ]); 
	 // ... 
 }; 
 

一旦 useEvent 被作为一个稳定的特性包含在 React 中,就像这样 避免 linter 限制 .

规则限制的最大问题是 React 将不再警告您该效果的依赖关系。 React 不会告诉你你的效果是否必须对你编写的新的响应式依赖项做出“反应”。例如,在前面的代码示例中,我们在依赖项中包含了 url 。 React 应该会提示你这样做。如果稍后编辑,禁用 linter 的效果将不会收到警告。这会产生错误。

以下代码是限制 linter 导致的错误示例。 handleMove 函数正在读取状态变量 canMove 的当前布尔值,以确定 Dot 组件是否会跟随光标。但是,在 handleMove 的主体中, canMove 的值始终为 true 。

  export   default   function   App  ()   { 
	 const   [  position  ,   setPosition  ]   =   useState  ({   x  :   0  ,   y  :   0   }); 
	 const   [  canMove  ,   setCanMove  ]   =   useState  (  true  ); 
	 const   handleMove   =   useCallback  ( 
		 ({   clientX  ,   clientY   }:   PointerEvent  )   =>   { 
			 if   (  canMove  )   { 
				 setPosition  ({   x  :   clientX  ,   y  :   clientY   }); 
			 } 
		 }, 
		 [  canMove  ] 
	 ); 
	 useEffect  (()   =>   { 
		 window  .  addEventListener  (  '  pointermove  '  ,   handleMove  ); 
		 return   ()   =>   window  .  removeEventListener  (  '  pointermove  '  ,   handleMove  ); 
		 // ? リンターに制限を加える 
		 // eslint-disable-next-line react-hooks/exhaustive-deps 
		 },   []); 
	 return   ( 
		 <  div   className  =  "App"  > 
			 {  /* ... */  } 
			 {  canMove   &&   <  Dot   position  =  {  position  }   />  } 
		 </  div  > 
	 ); 
 } 
 

问题是您限制了依赖项的 linter。如果取消限制,效果将是 handleMove
根据功能,您将被告知。 handleMove 是一个反应值,因为它是在组件主体内声明的。所有的反应值都必须包含在依赖项中。否则,当值改变时,效果的处理不会用旧值更新。

这个代码示例通过移除 linter 的限制“欺骗”了 React 效果没有反应性依赖 ( [] )。这就是为什么当 canMove (和 handleMove )发生变化时,React 没有重新同步效果。 React 不会重新同步效果,因此添加到事件 ( pointermove ) 的任何侦听器仍将是第一次渲染期间创建的 handleMove 函数。此时 canMove 的值为 true 。结果,不管 canMove 的值切换了多少, handleMove 继续看到 true 的原始值。

如果你不限制 linter,你就不会有过时值的问题 .以下示例 004 删除了 linter 限制并正确定义了依赖项 ( [handleMove] )。此外,限制 linter 的描述也被注释掉了。如果您有兴趣,请尝试一下。

示例 004 React + TypeScript:从效果中分离事件 04

使用 useEvent ,您不必“愚弄”linter,并且您的代码可以按预期工作。

示例 005 React + TypeScript:从效果中分离事件 05

useEvent 并不总能找到正确的解决方案。只有你不想反应的逻辑应该被切割成事件函数。例如,在上面的示例 005 中,我认为效果代码不应该对 canMove 产生反应。所以我把它剪成一个事件函数。

有关可以在不限制 lintering 的情况下正确定义效果依赖项的其他方式,请参阅 移除效果依赖 “请参阅。

事件函数的限制(实验 API)

[注意] 本节描述的 API 是实验性的。它还没有在 React 的常规版本中提供。

目前,事件函数的使用非常有限。

只能从效果内部调用。 不得传递给其他组件或挂钩。

例如,不要将声明的事件函数 ( onTick ) 传递给另一个钩子 ( useTimer ),如下所示:

src/Timer.tsx

  export   const   Timer  :   FC   =   ()   =>   { 
	 const   [  count  ,   setCount  ]   =   useState  (  0  ); 
	 const   onTick   =   useEvent  (()   =>   { 
		 setCount  (  count   +   1  ); 
	 }); 
	 useTimer  (  onTick  ,   1000  );   // ? NG: イベント関数を外に渡す 
	 return   <  h1  >  {  count  }  </  h1  >; 
 }; 
 

src/useTimer.ts

  export   const   useTimer   =   (  callback  :   ()   =>   void  ,   delay  :   number  )   =>   { 
	 useEffect  (()   =>   { 
		 const   id   =   setInterval  (()   =>   { 
			 callback  (); 
		 },   delay  ); 
		 return   ()   =>   { 
			 clearInterval  (  id  ); 
		 }; 
	 },   [  delay  ,   callback  ]);   // 依存関係にcallbackを含めなければならない 
 }; 
 

始终在与效果相同的位置声明事件函数,并从效果内调用它。各个模块的具体代码和动作请参见下面的示例006。

src/Timer.tsx

  export   const   Timer  :   FC   =   ()   =>   { 
	 const   [  count  ,   setCount  ]   =   useState  (  0  ); 
	 /* const onTick = useEvent(() => {
		setCount(count + 1);
	}); */ 
	 // useTimer(onTick, 1000); 
	 useTimer  (()   =>   { 
		 setCount  (  count   +   1  ); 
	 },   1000  ); 
	 return   <  h1  >  {  count  }  </  h1  >; 
 }; 
 

src/useTimer.ts

  export   const   useTimer   =   (  callback  :   ()   =>   void  ,   delay  :   number  )   =>   { 
	 const   onTick   =   useEvent  (()   =>   { 
		 callback  (); 
	 }); 
	 useEffect  (()   =>   { 
		 const   id   =   setInterval  (()   =>   { 
			 // callback(); 
			 onTick  ();   // ✅ OK: エフェクト内から内部的に呼び出す 
		 },   delay  ); 
		 return   ()   =>   { 
			 clearInterval  (  id  ); 
		 }; 
		 // }, [delay, callback]); 
	 },   [  delay  ]);   // onTick(イベント関数)は依存関係に含めなくてよい 
 }; 
 

示例 006 React + TypeScript:从效果中分离事件 06

将来,其中一些限制可能会被取消。但现在,最好将事件函数视为效果代码的非反应部分。因此,它应该与代码的效果密切相关。

概括

执行事件处理程序以响应某些交互。 只要需要同步,效果就会运行。 事件处理程序中的逻辑不是反应式的。 效果中的逻辑是被动的。 事件中的非反应性逻辑可以被切割成事件函数。 事件函数只能从效果中调用。 不要将事件函数传递给其他组件或挂钩。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308629346.html

查看更多关于React + TypeScript:将逻辑分离为事件和效果的详细内容...

  阅读:51次