我只想摆脱对useState和useEffect的依赖
React的文档分为旧版本和新版本。
我是在大约2019年开始接触React的,最近我又开始使用它,发现文档已经更新了。
有日文版本(主要部分已经翻译,但并非全部,如果不明白的话,最好同时使用原文)。
由于我对最近的前端情况不太了解,所以如果有任何错误,请告诉我,谢谢。
关于React
React是由Meta公司开发的前端库,通过称为”组件”的单元来管理UI、屏幕显示和数据控制。
React还提供了一种称为Hooks的独特功能,通过它可以在组件中使用各种功能。
使用useState来保持数据
用户进行的行动(例如表单输入)以及时间变化等需要保留和更新这些数据,才能实现动态网站。然而,仅仅像普通的代码一样的编写方式无法实现对这些数据的保留。
让我们考虑一个每次按下按钮数字就会增加的计数器。
export default function Count() {
let count = 0;
function handleClick() {
count = count + 1;
}
return (
<>
<button onClick={handleClick}>
{count} times clicked!
</button>
</>
);
}
只要试试在CodePen等平台上,就可以明白这段代码中的按钮显示次数不会更新!这是因为组件内的局部变量在渲染之间(即当前渲染和下一次渲染之间)不会保存数据。在此示例中,每次点击按钮时都会执行handleClick()函数,但由于局部变量的值不在渲染之间保存,所以渲染之间没有差异。React重新渲染是在渲染之间产生差异时执行的,因此在这里,对内部局部变量的更改不会成为渲染之间的差异,不会触发渲染。
在React中,useState提供了一种让组件在渲染之间保持”记忆”的功能。
import { useState } from 'react';
export default function Count() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<>
<button onClick={handleClick}>
{count} times clicked!
</button>
</>
);
}
我认为这个在中文中的原生解释如下:这段代码按照`const [a, b] = useState(initialValue)` 的语法可以使用,但通常更倾向于使用`[value, setValue]`这样的写法,其中0号元素用于保存数据,而1号元素用于向0号元素写入数据。通过这种方式可以以特定的形式保存数据。
使用useState的总结
-
- レンダー間で値を保持するのはuseState
変数名は[value, setValue]がおすすめ
与目标同步的useEffect
副作用的问题
在讨论useEffect之前,我想先在备忘录中提到副作用的含义。
“没有副作用”的函数指的是只执行纯粹计算的函数。这意味着无论多少次输入相同的值,结果都是一样的。
举个例子,假设有一个求最大公约数的函数gcd(a, b)。gcd(19, 57)无论被执行多少次,都应该返回19,而不是像某些著名数学家主张的返回1。
这样,输入和输出始终保持一致的函数被称为”没有副作用”的函数。
而”有副作用”的函数则指的是在执行时会依赖于环境,导致结果会变化的非纯粹计算。让我们考虑以下这样的函数。
let counter = 0;
function incrementCounter() {
counter++;
}
incrementCounter(); //1
incrementCounter(); //2
incrementCounter(); //3
//...
这个incrementCounter()函数每次执行时都会将counter的值增加1(递增),因此可以根据程序在执行时的状态来确定计算结果是否会改变。正因为如此,每次执行计算时都会导致状态的变化,因此无法保证相同的结果。这种具有副作用的函数被称为”具有副作用”的函数。
关于效果(Effect)
在这里,React开发团队将“指定由渲染本身而非特定事件引起的副作用”的称为“Effect”。说实话,我不太明白他们在说些什么。渲染本身引起的副作用,即与React渲染无关的与外部系统的交互被称为Effect……大概是这样吧。React应该只由没有副作用的计算组成,对于props和state应始终使用纯计算,而不是非React的小部件(如地图等)和与聊天室的连接之类的计算。这种副作用即使存在,也不是由特定用户事件引起的,被称为Effect,这些副作用在屏幕更新后,在提交的最后执行。而提供这种功能的是useEffect这个Hook。
我们来看一些具体的例子吧。这里我们使用官方的样本。
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]);
return (
<>
<label>
Server URL:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}
从’react’导入{ useState,useEffect };
从’./chat.js’导入{ createConnection };function ChatRoom({ roomId }) {
const [serverUrl,setServerUrl] = useState(‘https://localhost:1234’);
useEffect(() => {
const connection = createConnection(serverUrl,roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]);
return (
<>
欢迎来到{roomId}室!
</>
);
}
export default function App() {
const [roomId,setRoomId] = useState(‘general’);
const [show,setShow] = useState(false);
return (
<>
{show &&
}
{show && }
</>
);
}
chat.js
export function createConnection(serverUrl,roomId) {
// 实际实现应该连接到服务器
return {
connect() {
console.log(‘✅ 连接到”‘ + roomId + ‘”室,地址为’ + serverUrl + ‘…’);
},
disconnect() {
console.log(‘❌ 从”‘ + roomId + ‘”室断开连接,地址为’ + serverUrl);
}
};
}
useEffect接受两个参数,分别是“要执行的效果是什么”和“要订阅的变量数组”。在这个例子中,我们根据[roomId, serverUrl]的变化来执行连接和断开连接到非关键聊天室的效果。再深入一点讲,设置函数会在挂载时执行,清除函数会在卸载时执行。
useEffect(() => {
//ここがセットアップ関数
setup();
return cleanup();//returnで返却される関数がクリーンアップ関数
},[dependencies/*購読する変数の配列*/]);
在页面上添加组件时,首先会执行设置函数。在本示例中,当chatRoom被添加时,连接将被执行。
然后,当用户切换连接的房间并点击”Open Chat”时,会在渲染之间产生差异,这个差异自然是由useEffect订阅的值的更改引起的,因此将执行useEffect中的代码。
在这里,首先执行旧状态和prop的清理函数进行断开,然后执行新状态和prop的设置函数。
雖然我們已經進行了如此長的解釋,但是為了擺脫「僅僅使用useState和useEffect」的問題,首先需要重新理解「在哪種情境下需要使用這些Hooks」。如果只是把useEffect僅僅用於同步,這樣的想法就會導致浪費太多資源,或者生成運行緩慢的系統。
只在同步期使用useEffect。
只需一种选择时,以下是对上述内容的中文本机重新表述:
useEffect是用于与“外部系统”进行同步的,以处理效果的钩子。不建议在组件状态之间进行同步的情况下使用类似于效果的写法。
所以,在使用之前,应考虑同步系统的使用方式。
纵然如此,可能会存在一些在纯React计算中耗时较长的情况。可以通过缓存或记忆化等方式,在相同的输入下立即反映这些计算。
我想摆脱过去一直使用的代码,并且只使用useState和useEffect编写代码容易导致错误和性能下降的情况。
本文将使用React的内置钩子useRef和useCallback来实现SSE客户端。
SSE客户端的实现
SSE指的是什么?
SSE(Server-Sent Events)是一种功能,简而言之,服务器可以向已连接的客户端发送单向事件通知的能力,是服务器-客户端形式系统中双向通信的一种方式。
与同样是双向通信协议的WebSocket不同,SSE具有仅通过HTTP通信而完成以及在WS作为过度通信协议的场景中易于处理的优点。
在JavaScript中,可以将SSE视为一个EventSource。
const eventSource = new EventSource("https://example.com/sse");
eventSource.onmessage = function(msg) {
const data = JSON.parse(msg.data);
console.log(data);
}
eventSource.onerror = function(msg) {
console.log(msg);
}
通过这种方式,可以在订单商品菜单屏幕上即时反映商品售罄等服务器端才能知道的信息。
为了在React中使用SSE
如果要使用React来实现这个,用于反映这个的组件将会是什么?
-
- サーバにアクセスする、またSSEの接続を保持する
-
- SSEの通知を受け取りデータを更新(=再レンダリングさせる)
- なんらかの原因で接続のエラーが出た場合、復帰処理をする
需要这样的功能。是的,这将成为一个出色的效果。(实际上也是与外部系统的同步)
虽然如此,直接使用useEffect是不可取的。什么部分应该属于Effect,每次通知时进行繁重计算会导致绘制变慢的情况都是不理想的。
在中文中重新表述如下:此外,SSE的连接与画面变化没有任何关系。如果将这种连接作为状态进行保持的话,即使与屏幕显示无关,每次出现差异都会触发重新渲染,完全是多余的。
因此,在解决这些问题的同时,让我们去实施吧。
执行
因此,在实施过程中的要点如下:
-
- 純粋な計算と副作用のある計算を分ける
-
- 画面に反映するために必要な値はstate、関係の無い値はrefにする
- 接続・切断と必ず対になるので、セットアップ関数とクリーンアップ関数を記述する
您可以创建一个钩子函数,根据从服务器发出的SSE通知来返回同步的数据,并注意以下几点。
import { fetchData } from ‘../api/data’;
interface Data {
id
name
status
}
export function useEventSource() {
const [datas, setDatas] = useState([]);
const eventSourceRef = useRef(null);
const connectToEventSource = useCallback((eventSourceUrl) => {
if (eventSourceRef.current === null) {
const eventSource = new EventSource(eventSourceUrl);
eventSource.onmessage = function (event) {
console.log(‘事件: ‘, event.data);
fetchData().then((datas) => setDatas(datas));
}
eventSource.onerror = function (event) {
console.log(‘错误: ‘, event);
//重新连接
}
eventSourceRef.current = eventSource;
return;
}
}, []);
const closeEventSource = useCallback(() => {
if (eventSourceRef.current !== null) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
}, [eventSourceRef.current]);
useEffect(() => {
connectToEventSource(‘https://example.com/sse’);
}, []);
useEffect(() => {
console.log(‘获取数据中’);
fetchDatas().then((datas) => setDatas(datas));
}, []);
return [datas, setDatas] as const;
}
使用useRef
只需要一种选择,以下是对该句子的中文本地化改写:
`useRef是一个…`
-
- 値の変更によってレンダーを起こす必要のない値
- React外部のDOMの値
使用useRef可以在处理类似的事物时很方便。在官方文档中,useRef就像一个避难门,它是一个最佳的机制,用于”踏出去”,也就是用于保持这次的EventSource。
除此之外,还可以用于实现视频的播放和停止等功能,React正是在需要引用和操作外部系统时才会需要它。
使用 useCallback
只需一种选项– useCallback是
- 依存する値の変更がない限り関数の計算をキャッシュしたものを用いて再レンダーをスキップ
为了提高性能,我们将连接和断开处理封装成一个函数,并传递一个空数组来连接屏幕和SSE,因为在这种情况下并没有依赖值。