您可能已经熟悉React提供的一组内置Hooks,例如useState、useEffects、useMemo等。其中包括useSyncExternalStoreHook,它在库作者中非常常用,但在客户端React项目中很少见到。
useSyncExternalStore
useSyncExternalStore
如果你想订阅外部数据存储,可以是完美的应用编程接口。大多数时候,开发人员选择useEffect
挂钩。但是,如果你的数据存在于反应树之外,useSyncExternalStore
可能更合适。
基本的useSyncExternalStore
API包含三个参数:
typescript
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
让我们仔细看看这些参数:
-
subscribe
是一个回调/回传,它接受订阅外部存储数据的函数 -
getSnapshot
是一个函数,返回外部存储数据的当前快照 -
getServerSnapshot
是一个可选参数,它向您发送初始存储数据的快照。你可以在服务器数据的初始水化过程中使用它
useSyncExternalStore
返回您订阅的外部数据的当前快照。
考虑这样一种情况,即您有不在React树中的外部数据------换句话说,它存在于前端代码或应用程序之外。在这种情况下,您可以使用useSyncExternalStore
订阅该数据存储。
为了更好地理解useSyncExternalStore
钩子,让我们看一个非常简单的实现。您可以将其分配给一个变量(如下例中的list
),并根据需要将其渲染到UI:
typescript
import { useSyncExternalStore } from 'react';
import externalStore from './externalStore.js';
function Home() {
const list = useSyncExternalStore(externalStore.subscribe, externalStore.getSnapshot);
return (
<>
<section>
{list.map((itm, index) => (
<div key={index}>
<div>{itm?.title}</div>
</div>
))}
</section>
</>
);
}
如您所见,externalStore
现在已订阅,您将获得对externalStore
数据执行的任何更改的实时快照。您可以使用list
进一步映射来自外部源的项目并进行实时UI渲染。
外部存储中的任何更改都会立即反映出来,React将根据快照更改重新渲染UI。
useSyncExternalStore 场景
对于许多场景,useSyncExternalStore
都是理想的解决方案,例如:
-
从外部API缓存数据:由于此Hook主要用于订阅外部第三方数据源,因此缓存数据也变得更简单。您可以使应用程序的数据与外部数据源同步,以后也可以将其用于离线支持
-
WebSocket连接:由于WebSocket是一个"连续型"连接,您可以使用这个Hook来实时管理WebSocket连接状态数据
-
管理浏览器存储 :在这种情况下,您需要在Web浏览器的存储(如
IndexdDB
或localStorage
)和应用程序的状态之间同步数据,您可以使用useSyncExternalStore
订阅外部存储中的更新
在许多这样的情况下,这个钩子可能非常有用,并且比一直流行的useEffect
钩子更容易管理。让我们在下一节中比较这两个钩子。
useSyncExternalStore
与useEffect
您可以选择更常用的 useEffect
来实现类似于上面示例的功能:
typescript
const [list, setList] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
// assuming externalStore has a fetchData method or it is an async operation
const newList = await externalStore.fetchData();
setList(newList);
} catch (error) {
console.error(error);
}
};
// calling the async function here
fetchData();
}, []);
但是,useEffect
Hook不会为每个状态更新提供当前快照,并且它比useSyncExternalStore
Hook更容易出错。此外,它还受到其臭名昭著的重新渲染问题的困扰。接下来让我们简要回顾一下这个问题。
在处理useEffect
钩子时,您可能会遇到的一个主要问题是渲染顺序。浏览器完成绘制后,只有useEffect
钩子会触发。这种延迟------尽管是故意的------在管理正确的事件链时引入了意想不到的错误和挑战。
考虑以下示例:
jsx
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count- ', count);
// Imagine some asynchronous task here, like fetching data from an API
// This could introduce a delay between the state update and the effect running
// afterwards.
}, [count]);
const increment = () => {
setCount(count + 1);
};
console.log('outside the effect count - ', count);
return (
<div>
<div>Counter</div>
<div>Count: {count}</div>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
您可能期望计数器应用程序以简单的方式运行,其中状态更新,组件重新呈现,然后最终运行效果。然而,由于API调用的延迟,这里的事情变得有点棘手,事件的顺序可能不是我们所期望的。
现在考虑一个有许多这样的副作用和不同依赖数组的应用程序。在这种情况下,用正确的顺序跟踪状态更新将是一场噩梦。
如果您的数据位于外部,并且不依赖于现有的React API来处理,那么您可以避免所有这些,并使用useSyncExternalStore
钩子来修复此性能差距。与useEffect
钩子不同,此钩子会立即触发,不会造成延迟。
useSyncExternalStore
还可以防止前面提到的重新渲染问题,每当状态发生变化时,您可能会面临useEffect
。有趣的是,订阅了useSyncExternalStore
的状态不会重新渲染两次,从而修复了巨大的性能问题。
useSyncExternalStore
vs.useState
在使用useSyncExternalStore
钩子时,您可能会觉得您只是订阅一个状态并将其分配给一个变量,类似于 useState
。然而,useSyncExternalStore
不仅仅是分配状态。
使用状态挂钩的useState
限制是它被设计为以"每个组件"的方式管理状态。换句话说,你定义的状态仅限于它自己的反应组件,不能全局访问。你可以使用回调、全局强制状态,甚至在整个组件中使用道具钻取状态,但这可能会减慢你的反应应用程序。
这个useSyncExternalStore
钩子通过设置一个全局状态来防止这个问题,你可以从任何React组件订阅它,不管它有多深嵌套。更好的是,如果你在处理一个非React代码库,你只需要关心订阅事件。
useSyncExternalStore
将向您发送可以在任何React组件中使用的全局存储当前状态的正确快照。
构建待办事项应用程序useSyncExternalStore
让我们通过构建一个演示待办事项应用程序来看看useSyncExternalStore
钩子在实际项目中有多有用。首先,创建一个store.js
文件,该文件将充当外部全局状态。我们稍后将为我们的待办事项订阅此状态:
javascript
let todos = [];
let subscribers = new Set();
const store = {
getTodos() {
// getting all todos
return todos;
},
// subscribe and unsubscribe from the store using callback
subscribe(callback) {
subscribers.add(callback);
return () => subscribers.delete(callback);
},
// adding todo to the state
addTodo(text) {
todos = [
...todos,
{
id: new Date().getTime(),
text: text,
completed: false,
},
];
subscribers.forEach((callback) => {
callback();
});
},
// toggle for todo completion using id
toggleTodo(id) {
todos = todos.map((todo) => {
return todo.id === id ? { ...todo, completed: !todo.completed } : todo;
});
subscribers.forEach((callback) => callback());
},
};
// exporting the default store state
export default store;
您的store
现在已准备好在React组件中订阅。继续创建一个简单的Todo
组件,通过订阅您之前创建的商店将待办事项渲染到UI:
jsx
import { useSyncExternalStore } from "react";
import store from "./store.js";
function Todo() {
// subscribing to the store
const todosStore = useSyncExternalStore(store.subscribe, store.getTodos);
return (
<div>
{todosStore.map((todo, index) => (
<div key={index}>
<input
type="checkbox"
value={todo.completed}
onClick={() => store.toggleTodo(todo.id)}
/>
// toggle based on completion logic
{todo.completed ? <div>{todo.text}</div> : todo.text}
</div>
))}
</div>
);
}
export default Todo;
这样,我们使用useSyncExternalStore的迷你演示项目就完成了。结果应该如下所示: