useSnapshot

预计阅读时间: 5 分钟

订阅 State 容器的钩子。

useSnapshot 是专门为 State 容器设计的高级订阅钩子。它使用 useSyncExternalStore 来确保在并发模式下安全地订阅状态变更,并提供两个重载——不带选择器用于完整状态访问,或带选择器用于派生状态订阅。

示例

不带选择器

import { useSnapshot } from '@videojs/react';
import { stateContainer } from './myContainer';

function FullState() {
  const state = useSnapshot(stateContainer);

  // state 是完整的 State 类型
  return (
    <div>
      <p>Current time: {state.currentTime}</p>
      <p>Duration: {state.duration}</p>
      <p>Paused: {String(state.paused)}</p>
    </div>
  );
}

带选择器

import { useSnapshot } from '@videojs/react';
import { stateContainer } from './myContainer';

function DerivedState() {
  // 选择并派生状态
  const { remaining, progress } = useSnapshot(
    stateContainer,
    (state) => ({
      remaining: state.duration - state.currentTime,
      progress: state.duration > 0 
        ? (state.currentTime / state.duration) * 100 
        : 0,
    })
  );

  return (
    <div>
      <p>Remaining: {remaining.toFixed(1)}s</p>
      <p>Progress: {progress.toFixed(1)}%</p>
    </div>
  );
}

在播放器中使用

import { createPlayer, useSnapshot } from '@videojs/react';

const Player = createPlayer({ /* config */ });

function PlaybackInfo() {
  const state = Player.useSnapshot((s) => ({
    time: s.currentTime,
    duration: s.duration,
    buffered: s.buffered,
  }));

  return (
    <div className="info-panel">
      <span>{state.time.toFixed(1)}s</span>
      <span>/</span>
      <span>{state.duration.toFixed(1)}s</span>
    </div>
  );
}

API 参考

不带选择器

订阅完整的 State 容器状态。

参数

参数类型默认值说明
containerStateContainer<S>State 容器实例

返回值

S - 完整的 State 类型

带选择器

从 State 容器中选择并订阅派生值。

参数

参数类型默认值说明
containerStateContainer<S>State 容器实例
selector(state: S) => R从状态中选择派生值的函数
isEqual(a: R, b: R) => booleanshallowEqual相等比较函数

返回值

R - 选择器返回的派生值类型

State 容器 vs Store

State 容器

StateContainer 是纯状态的容器,只包含状态数据:

interface StateContainer<S> {
  subscribe: (callback: () => void) => () => void;
  getState: () => S;
}

Store

Store 在 State 容器基础上增加了操作方法:

interface Store<S, A> extends StateContainer<S> {
  dispatch: (action: A) => void;
  // ... 操作方法如 play(), pause(), seek()
}

选择建议

场景推荐说明
只需读取状态useSnapshot更轻量,无需操作方法
需要执行操作useStore包含状态和操作
构建自定义钩子useSelector最底层,最灵活

与 useStore 的关系

useStoreuseSnapshot 的基础上增加了存储实例的返回:

// useSnapshot - 只返回状态
const state = useSnapshot(container, selector);

// useStore - 同时返回状态和存储实例
const { state, store } = useStore(storeInstance, selector);

当只需要订阅状态而不需要直接调用存储方法时,useSnapshot 是更好的选择。

与 useSelector 的关系

useSnapshotuseSelector 的高级封装,专门为 State 容器设计:

// useSnapshot 内部实现
function useSnapshot<S, R>(
  container: StateContainer<S>,
  selector?: (state: S) => R,
  isEqual: Comparator<R> = shallowEqual
) {
  return useSelector(
    container.subscribe,
    () => container.getState(),
    selector,
    isEqual
  );
}

主要区别

特性useSelectoruseSnapshot
通用性通用(任何订阅源)专用(State 容器)
默认比较器Object.isshallowEqual
API 简洁性需要提供 subscribe 和 getSnapshot直接使用容器

使用建议

  • 对于 State 容器,使用 useSnapshot 更简洁
  • 对于自定义订阅源,使用 useSelector
  • 对于 Store,使用 useStore 以获得操作方法

相等性比较

默认行为

useSnapshot 默认使用 shallowEqual 进行浅相等比较:

// 默认浅相等比较
const derived = useSnapshot(container, (state) => ({
  time: state.currentTime,
  label: state.label,
}));

自定义比较

可以提供自定义的相等函数:

// 使用深度相等
const deep = useSnapshot(
  container,
  (state) => state.data,
  deepEqual // 自定义深度比较
);

// 使用严格相等
const exact = useSnapshot(
  container,
  (state) => state.id,
  Object.is // 严格相等(默认行为)
);

使用场景

派生计算状态

function ComputedState() {
  const derived = useSnapshot(container, (state) => ({
    // 计算进度百分比
    progress: state.duration > 0 
      ? (state.currentTime / state.duration) * 100 
      : 0,
    // 计算剩余时间
    remaining: state.duration - state.currentTime,
    // 判断是否在播放
    isPlaying: !state.paused && state.duration > 0,
  }));

  return (
    <div>
      <div className="progress-bar" style={{ width: `${derived.progress}%` }} />
      <span>{derived.remaining.toFixed(1)}s remaining</span>
      <span>{derived.isPlaying ? 'Playing' : 'Stopped'}</span>
    </div>
  );
}

条件渲染

function ConditionalRender() {
  const isReady = useSnapshot(
    container,
    (state) => state.readyState >= 3
  );

  if (!isReady) {
    return <Spinner />;
  }

  return <VideoPlayer />;
}

参见