【译】如何在 React 18 中使用 useSyncExternalStore

原文

useSyncExternalStore是 React 18 中提供的自定义挂钩,可让您订阅外部存储并在外部存储更新时更新您的 React 组件。

它对于订阅不是建立在 React 状态管理之上的外部存储特别有用。

useSyncExternalStore API

您应该在组件的顶层调用useSyncExternalStore方法

javascript 复制代码
import { useSyncExternalStore } from 'react';
import { myStore } from "./mystore.js";

function MyComponent() {
	const data = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
    // Rest of the component ...
}

useSyncExternalStore方法接受两个参数:

  • subscribe- subscribe 方法应该订阅商店更新,并且应该返回一个函数来取消订阅商店更新。我们将通过下面的示例了解如何创建与useSyncExternalStore.
  • getSnapshot- 方法将从存储返回数据的快照。

useSyncExternalStore基本示例使用

让我们构建一个非常基本的商店来了解useSyncExternalStore API。

我们的商店将仅存储一个计数,并提供一种递增和递减计数的方法。

javascript 复制代码
  let count = 0; // Variable to store count
  let subscribers = new Set(); // Set to store callback functions

  const countStore = {
    read() {
      // Method to get the count, this is basically getSnapshot method.
      return count;
    },
    // Subscribe method adds the "callback" to the "subscribers" set, and
    // return a method to unsubscribe from the store.
    subscribe(callback) {
      subscribers.add(callback);
      return () => subscribers.delete(callback);
    },
    // Method to increment the count
    increment() {
      count++;
      subscribers.forEach((callback) => callback());
    },
    decrement() {
      count--;
      subscribers.forEach((callback) => callback());
    },
  };

export default countStore;

count用于存储我们的计数器,数组用于subscribers存储订阅者方法的列表。

read()方法是getSnapshot()获取存储快照的方法。

subscribe(callback)方法用于订阅商店,并且采用useSyncExternalStore.

我们将该callback方法存储在一个 Set 中,每次更新计数时,我们都会迭代所有方法callback并调用它们。

一旦被callback调用,它将useSyncExternalStore调用该read()方法从存储中获取值。

现在,让我们构建组件来使用商店,我们将更新App.js组件以使用新创建的商店。

javascript 复制代码
import "./App.css";
import countStore from "./store";
import { useSyncExternalStore } from "react";

function App() {
  const count = useSyncExternalStore(countStore.subscribe, countStore.read);

  return (
    <div className="App">
      count: {count}
      <div>
        <button onClick={countStore.increment}>Increment</button>
        <button onClick={countStore.decrement}>Decrement</button>
      </div>
    </div>
  );
}

export default App;

使用useSyncExternalStore构建一个Todo应用

让我们创建一个 Todo 应用程序,首先,我们将创建一个store.js用于存储待办事项的文件:

javascript 复制代码
let todos = [];
let subscribers = new Set();

const store = {
  getTodos() {
    // Method to get the todos array.
    return todos;
  },
  // Subscribe method adds the "callback" to the "subscribers" set, and
  // return a method to unsubscribe from the store.
  subscribe(callback) {
    subscribers.add(callback);
    return () => subscribers.delete(callback);
  },
  addTodo(text) {
    todos = [
      ...todos,
      {
        id: new Date().getTime(),
        text: text,
        completed: false,
      },
    ];

    subscribers.forEach((callback) => {
      callback();
    });
  },
  toggleTodo(id) {
    todos = todos.map((todo) => {
      return todo.id === id ? { ...todo, completed: !todo.completed } : todo;
    });
    subscribers.forEach((callback) => callback());
  },
};

export default store;

在我们的store.js文件中,我们创建了一个名为 as 的数组todos来存储待办事项列表。

接下来与之前的示例类似,我们创建了一个名为 as 的变量subscribers,其中包含已订阅商店的回调函数数组。

每当存储的值更新时,我们就必须调用这些函数。

在该addTodo方法中,您可能已经注意到,我们没有使用push方法将待办事项添加到数组中todos,因为通过这样做,React 将不会检测到更改并重新加载组件,因为 React 中的不变性。

因此,您应该记住不要todo就地更新数组,而是在添加待办事项时创建一个新数组。

我们在方法中做同样的事情toggleTodo,而不是在方法中按索引更新待办事项,而是使用包含更新值的方法toggleTodo创建一个新todos数组。map

接下来,我们将创建一个TodoList.js文件来保存我们的TodoList组件,该TodoList组件将使用useSyncExternalStore订阅存储并在添加新的 Todo 时更新其 UI。

javascript 复制代码
import { useSyncExternalStore } from "react";
import store from "./store";
function TodoList() {
  const todos = useSyncExternalStore(store.subscribe, store.getTodos);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              value={todo.completed}
              onClick={() => store.toggleTodo(todo.id)}
            />
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </label>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

在我们的组件中,我们使用钩子从商店中TodoList获取列表。todosuseSyncExternalStore

toggleTodo当检查待办事项时,我们也会调用该方法,当我们运行此代码时,您将看到 UI 将被更新。

接下来,我们将创建一个组件,以添加新的 Todo,这是我们组件AddTodoForm的代码:AddTodoForm

javascript 复制代码
import store from "./store";
import { useState } from "react";
function AddTodoForm() {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    store.addTodo(text);
    setText("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button type="submit">Add Todo</button>
    </form>
  );
}

export default AddTodoForm;

AddTodoForm组件也非常基本,这里我们导入我们的商店,并使用useStatereact中的方法。

我们创建了一个名为 as 的状态变量,text它将存储待办事项的文本。

接下来,我们创建了两个事件监听器,我们在标签上添加了一个onChange事件监听器<input />,当输入标签的值发生变化时,我们将使用text在输入标签上键入的值更新名为 as 的状态变量。

onSubmit我们添加到表单的第二个事件侦听器是在按下"添加待办事项"按钮提交表单时添加的事件侦听器。

onSubmit事件监听器中我们正在调用handleSubmit方法,这个方法,这个方法会调用addTodo我们Store的方法。

当从此组件添加待办事项时,由于我们useSyncExternalStore在组件中使用钩子TodoListTodoList该组件也会自动更新以显示新添加的待办事项。

现在,让我们最终构建我们的App.js组件,并导入我们的AddTodoForm和TodoList组件:

javascript 复制代码
import React, { useState } from "react";

import AddTodoForm from "./AddTodoForm";
import TodoList from "./TodoList";

function App() {
  return (
    <div>
      <h1>Todo App</h1>
      <AddTodoForm />
      <TodoList />
    </div>
  );
}

export default App;

使用自定义钩子改进代码

我们可以进一步改进我们的代码,但在自定义挂钩中提取调用脚趾useSyncExternalStore,然后在我们的组件中简单地使用自定义挂钩。

我们将更新我们的store.js文件以包含我们的自定义挂钩的代码,称为useTodo

javascript 复制代码
import { useSyncExternalStore } from "react";

// Custom Hook useTodo
export function useTodo() {
const todos = useSyncExternalStore(store.subscribe, store.getTodos);
return todos;
} 

然后我们将更新我们的TodoList.js文件以包含我们的自定义useTodo挂钩:

javascript 复制代码
import store, { useTodo } from "./store";

function TodoList() {
  const todos = useTodo();
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              value={todo.completed}
              onClick={() => store.toggleTodo(todo.id)}
            />
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </label>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

什么时候使用useSyncExternalStore

建议您使用 React 内置的状态管理钩子(如useState和 )useReducer来管理状态。

但有一些场景是useSyncExternalStore有意义的:

将 React 与现有的非 React 代码库集成

如果您有一个使用某种类型的外部存储的非 React 代码库,并且您希望将 React 应用程序与现有存储集成,那么在这种情况下,您可以围绕存储构建一个与 API 一致的包装器以无缝useSyncExternalStore集成带有 React 应用程序的商店。

订阅浏览器 API

您可以使用它来订阅浏览器 API,例如 Web 推送通知或属性navigator.onLine

React 官方文档有一个很好的例子,解释了如何使用hooknavigator.onLine属性useSyncExternalStore

结论

在这篇博文中,我们学习了如何useSyncExternalStore在代码中使用 React 与外部存储同步。

我们应该尽可能在我们的代码中使用useStateand useReducerhooks,但在少数情况下不可能使用useStateanduseReducer我们可以使用 the useSyncExternalStore将我们的 React 组件与外部存储同步。

相关推荐
wearegogog1237 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars8 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤8 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·8 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°8 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_419854059 小时前
CSS动效
前端·javascript·css
烛阴9 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪9 小时前
markstream-vue实战踩坑笔记
前端
C_心欲无痕9 小时前
nginx - 实现域名跳转的几种方式
运维·前端·nginx