在撰写本文时,“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:将逻辑分离为事件和效果的详细内容...