likes
comments
collection
share

[译]探索useSyncExternalStore,一个鲜为人知的React Hook

作者站长头像
站长
· 阅读数 4

您可能已经熟悉React提供的一组内置Hooks,例如useState、useEffects、useMemo等。其中包括useSyncExternalStoreHook,它在库作者中非常常用,但在客户端React项目中很少见到。

[译]探索useSyncExternalStore,一个鲜为人知的React Hook

useSyncExternalStore

useSyncExternalStore如果你想订阅外部数据存储,可以是完美的应用编程接口。大多数时候,开发人员选择useEffect挂钩。但是,如果你的数据存在于反应树之外,useSyncExternalStore可能更合适。

基本的useSyncExternalStoreAPI包含三个参数:

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)

让我们仔细看看这些参数:

  • subscribe是一个回调/回传,它接受订阅外部存储数据的函数

  • getSnapshot是一个函数,返回外部存储数据的当前快照

  • getServerSnapshot是一个可选参数,它向您发送初始存储数据的快照。你可以在服务器数据的初始水化过程中使用它

useSyncExternalStore返回您订阅的外部数据的当前快照。

考虑这样一种情况,即您有不在React树中的外部数据——换句话说,它存在于前端代码或应用程序之外。在这种情况下,您可以使用useSyncExternalStore订阅该数据存储。

为了更好地理解useSyncExternalStore钩子,让我们看一个非常简单的实现。您可以将其分配给一个变量(如下例中的list),并根据需要将其渲染到UI:

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浏览器的存储(如 IndexdDBlocalStorage)和应用程序的状态之间同步数据,您可以使用useSyncExternalStore订阅外部存储中的更新

在许多这样的情况下,这个钩子可能非常有用,并且比一直流行的useEffect钩子更容易管理。让我们在下一节中比较这两个钩子。

useSyncExternalStoreuseEffect

您可以选择更常用的 useEffect 来实现类似于上面示例的功能:

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();
  }, []);

但是,useEffectHook不会为每个状态更新提供当前快照,并且它比useSyncExternalStoreHook更容易出错。此外,它还受到其臭名昭著的重新渲染问题的困扰。接下来让我们简要回顾一下这个问题。

在处理useEffect钩子时,您可能会遇到的一个主要问题是渲染顺序。浏览器完成绘制后,只有useEffect钩子会触发。这种延迟——尽管是故意的——在管理正确的事件链时引入了意想不到的错误和挑战。

考虑以下示例:

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的状态不会重新渲染两次,从而修复了巨大的性能问题。

useSyncExternalStorevs.useState

在使用useSyncExternalStore钩子时,您可能会觉得您只是订阅一个状态并将其分配给一个变量,类似于 useState 。然而,useSyncExternalStore不仅仅是分配状态。

使用状态挂钩的useState限制是它被设计为以“每个组件”的方式管理状态。换句话说,你定义的状态仅限于它自己的反应组件,不能全局访问。你可以使用回调、全局强制状态,甚至在整个组件中使用道具钻取状态,但这可能会减慢你的反应应用程序。

这个useSyncExternalStore钩子通过设置一个全局状态来防止这个问题,你可以从任何React组件订阅它,不管它有多深嵌套。更好的是,如果你在处理一个非React代码库,你只需要关心订阅事件。

useSyncExternalStore将向您发送可以在任何React组件中使用的全局存储当前状态的正确快照。

构建待办事项应用程序useSyncExternalStore

让我们通过构建一个演示待办事项应用程序来看看useSyncExternalStore钩子在实际项目中有多有用。首先,创建一个store.js文件,该文件将充当外部全局状态。我们稍后将为我们的待办事项订阅此状态:

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:

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的迷你演示项目就完成了。结果应该如下所示:

[译]探索useSyncExternalStore,一个鲜为人知的React Hook

原文