当前位置: 首页 > article >正文

React 中hooks之useSyncExternalStore使用总结

1. 基本概念

useSyncExternalStore 是 React 18 引入的一个 Hook,用于订阅外部数据源,确保在并发渲染下数据的一致性。它主要用于:

  • 订阅浏览器 API(如 window.width)
  • 订阅第三方状态管理库
  • 订阅任何外部数据源

1.1 基本语法

const state = useSyncExternalStore(
  subscribe,  // 订阅函数
  getSnapshot, // 获取当前状态的函数
  getServerSnapshot // 可选:服务端渲染时获取状态的函数
);

2. 基础示例

2.1 订阅窗口大小变化

getSnapshot 是一个函数,用于返回当前浏览器窗口的宽度和高度。window.innerWidth 和 window.innerHeight 分别获取浏览器窗口的宽度和高度。
该函数返回一个对象,包含 width 和 height 两个属性。
subscribe 函数接受一个回调函数 callback,并将其作为事件监听器绑定到 resize 事件上。
每当浏览器窗口的尺寸发生变化时,resize 事件会触发,进而调用 callback。
subscribe 函数还返回一个清理函数(return () => window.removeEventListener(‘resize’, callback)),用于在组件卸载时移除事件监听器,防止内存泄漏。
当callback回调触发的时候就会触发组件更新

function useWindowSize() {
  const getSnapshot = () => ({
    width: window.innerWidth,
    height: window.innerHeight
  });

  const subscribe = (callback) => {
    window.addEventListener('resize', callback);
    return () => window.removeEventListener('resize', callback);
  };

  return useSyncExternalStore(subscribe, getSnapshot);
}

function WindowSizeComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      Window size: {width} x {height}
    </div>
  );
}

2.2 订阅浏览器在线状态

function useOnlineStatus() {
  const getSnapshot = () => navigator.onLine;

  const subscribe = (callback) => {
    window.addEventListener('online', callback);
    window.addEventListener('offline', callback);
    
    return () => {
      window.removeEventListener('online', callback);
      window.removeEventListener('offline', callback);
    };
  };

  return useSyncExternalStore(subscribe, getSnapshot);
}

function OnlineStatusComponent() {
  const isOnline = useOnlineStatus();

  return (
    <div>
      Status: {isOnline ? '在线' : '离线'}
    </div>
  );
}

3. 进阶用法

3.1 创建自定义存储

useTodoStore 是一个自定义 Hook,它使用了 useSyncExternalStore 来同步外部存储(即 todoStore)的状态。
todoStore.subscribe:订阅状态更新。每当 todoStore 中的状态变化时,useSyncExternalStore 会触发重新渲染。
todoStore.getSnapshot:获取当前的状态快照,在此返回的对象包含 todos 和 filter。

function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();

  return {
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return state;
    },
    setState(newState) {
      state = newState;
      listeners.forEach(listener => listener());
    }
  };
}

const todoStore = createStore({
  todos: [],
  filter: 'all'
});

function useTodoStore() {
  return useSyncExternalStore(
    todoStore.subscribe,
    todoStore.getSnapshot
  );
}

function TodoList() {
  const { todos, filter } = useTodoStore();

  return (
    <ul>
      {todos
        .filter(todo => filter === 'all' || todo.completed === (filter === 'completed'))
        .map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
    </ul>
  );
}

3.2 与服务端渲染集成

function useSharedState(initialState) {
  const store = useMemo(() => createStore(initialState), [initialState]);

  // 提供服务端快照
  const getServerSnapshot = () => initialState;

  return useSyncExternalStore(
    store.subscribe,
    store.getSnapshot,
    getServerSnapshot
  );
}

3.3 订阅 WebSocket 数据

function useWebSocketData(url) {
  const [store] = useState(() => {
    let data = null;
    const listeners = new Set();
    
    const ws = new WebSocket(url);
    ws.onmessage = (event) => {
      data = JSON.parse(event.data);
      listeners.forEach(listener => listener());
    };

    return {
      subscribe(listener) {
        listeners.add(listener);
        return () => {
          listeners.delete(listener);
          if (listeners.size === 0) {
            ws.close();
          }
        };
      },
      getSnapshot() {
        return data;
      }
    };
  });

  return useSyncExternalStore(store.subscribe, store.getSnapshot);
}

function LiveDataComponent() {
  const data = useWebSocketData('wss://api.example.com/live');

  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <h2>实时数据</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

4. 性能优化

4.1 选择性订阅

function useStoreSelector(selector) {
  const store = useContext(StoreContext);
  
  const getSnapshot = useCallback(() => {
    return selector(store.getSnapshot());
  }, [store, selector]);

  return useSyncExternalStore(
    store.subscribe,
    getSnapshot
  );
}

// 使用示例
function TodoCounter() {
  const count = useStoreSelector(state => state.todos.length);
  return <div>Total todos: {count}</div>;
}

4.2 避免不必要的更新

function createStoreWithSelector(initialState) {
  let state = initialState;
  const listeners = new Map();

  return {
    subscribe(listener, selector) {
      const wrappedListener = () => {
        const newSelectedValue = selector(state);
        if (newSelectedValue !== selector(previousState)) {
          listener();
        }
      };
      listeners.set(listener, wrappedListener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return state;
    },
    setState(newState) {
      const previousState = state;
      state = newState;
      listeners.forEach(listener => listener());
    }
  };
}

5. 实际应用场景

5.1 主题切换系统

function createThemeStore() {
  let theme = 'light';
  const listeners = new Set();

  return {
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return theme;
    },
    toggleTheme() {
      theme = theme === 'light' ? 'dark' : 'light';
      listeners.forEach(listener => listener());
    }
  };
}

const themeStore = createThemeStore();

function useTheme() {
  return useSyncExternalStore(
    themeStore.subscribe,
    themeStore.getSnapshot
  );
}

function ThemeToggle() {
  const theme = useTheme();

  return (
    <button onClick={() => themeStore.toggleTheme()}>
      Current theme: {theme}
    </button>
  );
}

5.2 表单状态管理

function createFormStore(initialValues) {
  let values = initialValues;
  const listeners = new Set();

  return {
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return values;
    },
    updateField(field, value) {
      values = { ...values, [field]: value };
      listeners.forEach(listener => listener());
    },
    reset() {
      values = initialValues;
      listeners.forEach(listener => listener());
    }
  };
}

function useForm(initialValues) {
  const [store] = useState(() => createFormStore(initialValues));
  
  return useSyncExternalStore(
    store.subscribe,
    store.getSnapshot
  );
}

function Form() {
  const formData = useForm({ name: '', email: '' });

  return (
    <form>
      <input
        value={formData.name}
        onChange={e => formStore.updateField('name', e.target.value)}
      />
      <input
        value={formData.email}
        onChange={e => formStore.updateField('email', e.target.value)}
      />
    </form>
  );
}

6. 注意事项

  1. 保持一致性

    • subscribe 函数应该返回清理函数
    • getSnapshot 应该返回不可变的数据
  2. 避免频繁更新

    • 考虑使用节流或防抖
    • 实现选择性订阅机制
  3. 服务端渲染

    • 提供 getServerSnapshot
    • 确保服务端和客户端状态同步
  4. 内存管理

    • 及时清理订阅
    • 避免内存泄漏

通过合理使用 useSyncExternalStore,我们可以安全地订阅外部数据源,并确保在 React 并发渲染下的数据一致性。这个 Hook 特别适合需要与外部系统集成的场景。 还可以用来实现浏览器localStrorage的持久化存储


http://www.kler.cn/a/514518.html

相关文章:

  • 【机器学习实战中阶】使用SARIMAX,ARIMA预测比特币价格,时间序列预测
  • Asp.Net Core 8.0 使用 Serilog 按日志级别写入日志文件的两种方式
  • < OS 有关 > 阿里云:轻量应用服务器 的使用 安装 Tailscale 后DNS 出错, 修复并替换 apt 数据源
  • 以Python构建ONE FACE管理界面:从基础至进阶的实战探索
  • R语言的图形用户界面
  • Qt中自定义信号与槽
  • NS3网络模拟器中如何利用Gnuplot工具像MATLAB一样绘制各类图形?
  • Vue - ref( ) 和 reactive( ) 响应式数据的使用
  • 22.日常算法
  • stm8s单片机(一) 工程塔建与第一个实验程序
  • 漏洞情报:为什么、要什么和怎么做
  • CrypTen——基于pytorch的隐私保护机器学习框架
  • Git进阶笔记系列(01)Git核心架构原理 | 常用命令实战集合
  • Julia语言的区块链
  • Java设计模式 三 工厂方法模式 (Factory Method Pattern)
  • HTML 基础入门:核心标签全解析
  • 深圳大学-计算机系统(3)-实验三取指和指令译码设计
  • simulink入门学习01
  • Redis、MongoDB 和 MySQL评估
  • IBM湖仓一体与向量数据库:访问MinIO控制台(Accessing the MinIO console)
  • AI对齐与开源发展:多学科融合创新之路
  • 第二讲 矩阵消元——用矩阵的左乘表示矩阵消元的过程
  • Spring注解篇:@RequestMapping详解
  • ESP-Mesh-Lite组网方案,赋能设备多场景联网通信,无线交互控制应用
  • PHP常见正则表达式
  • 不用安装双系统,如何在mac上玩windows游戏呢?