自学内容网 自学内容网

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的持久化存储


原文地址:https://blog.csdn.net/qq_34645412/article/details/145298928

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!