useSelector

预计阅读时间: 5 分钟

低级钩子,用于订阅派生状态。

useSelector 是 Video.js React 中的低级订阅钩子,它使用 useSyncExternalStore 来确保在并发模式下安全地订阅外部状态。这个钩子是构建更高级钩子的基础,如 useStoreusePlayer

示例

基本用法

import { useSelector } from '@videojs/react';
import { store } from './myStore';

function SelectedValue() {
  const value = useSelector(
    store.subscribe,
    () => store.getState(),
    (state) => state.selectedId
  );

  return <div>Selected: {value}</div>;
}

自定义相等函数

import { useSelector } from '@videojs/react';
import { store } from './myStore';

function PlayerState() {
  // 使用浅相等比较
  const { paused, currentTime, duration } = useSelector(
    store.subscribe,
    () => store.getState(),
    (state) => ({
      paused: state.paused,
      currentTime: state.currentTime,
      duration: state.duration,
    }),
    (a, b) => shallowEqual(a, b) // 自定义相等函数
  );

  return (
    <div>
      <p>Paused: {String(paused)}</p>
      <p>Time: {currentTime.toFixed(1)}s / {duration.toFixed(1)}s</p>
    </div>
  );
}

与 useStore 的关系

useStore 实际上是 useSelector 的高级封装:

// useStore 内部实现(简化)
function useStore<S, R>(store: S, selector?: SelectorFn, isEqual?: Comparator) {
  return useSelector(
    store.subscribe,
    () => store.getState(),
    selector,
    isEqual
  );
}

// 直接使用 useSelector
function CustomHook() {
  const result = useSelector(
    myStore.subscribe,
    () => myStore.getState(),
    (state) => computeDerivedValue(state),
    deepEqual
  );
  
  return result;
}

API 参考

参数

参数类型默认值说明
subscribeSubscribeFn订阅函数,接收回调并在变更时调用
getSnapshot() => T获取当前快照的函数
selector(state: T) => R从状态中选择派生值的函数
isEqual(a: R, b: R) => boolean自定义相等比较函数

SubscribeFn

type SubscribeFn = (callback: () => void) => () => void

订阅函数应返回取消订阅的函数。

getSnapshot

type GetSnapshotFn<T> = () => T

返回状态的当前快照。对于状态容器,这通常是 getState()

selector

type SelectorFn<T, R> = (state: T) => R

从状态中选择派生值。如果不提供,将返回整个状态。

isEqual

type Comparator<R> = (a: R, b: R) => boolean

用于比较新旧值是否相等的函数。如果返回 true,组件不会重新渲染。

返回值

返回值类型说明
Rany选择器返回的派生值

当不提供选择器时,返回类型为 T(状态的完整快照)。

相等性比较

默认比较器

如果不提供 isEqual 参数,默认使用 Object.is 进行严格相等比较:

// 默认严格相等
const value = useSelector(
  store.subscribe,
  () => store.getState(),
  (state) => state.counter // 只有当 counter 引用改变时才重新渲染
);

浅相等比较

Video.js React 提供了 shallowEqual 函数用于浅相等比较:

import { useSelector, shallowEqual } from '@videojs/react';

const selected = useSelector(
  store.subscribe,
  () => store.getState(),
  (state) => ({ id: state.id, label: state.label }),
  shallowEqual // 当所有属性的值相等时才重新渲染
);

自定义比较器

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

import { useSelector } from '@videojs/react';

// 深度相等比较
const data = useSelector(
  store.subscribe,
  () => store.getState(),
  (state) => state.data,
  deepEqual // 自定义深度比较
);

// 特定字段比较
const hasChanged = useSelector(
  store.subscribe,
  () => store.getState(),
  (state) => state.timestamp,
  (a, b) => Math.abs(a - b) < 1000 // 1秒内的变化视为相同
);

与 useStore/useSnapshot 的关系

useSnapshot

useSnapshot 是专门为 State 容器设计的 useSelector 封装:

// useSnapshot 内部使用 useSelector
function useSnapshot(container, selector) {
  return useSelector(
    container.subscribe,
    () => container.getState(),
    selector,
    shallowEqual
  );
}

useStore

useStoreuseSelector 基础上增加了存储实例的直接访问:

// useStore 同时返回状态和存储实例
function useStore<S, R>(store: S, selector?: SelectorFn, isEqual?: Comparator) {
  const state = useSelector(
    store.subscribe,
    () => store.getState(),
    selector,
    isEqual
  );
  
  // 额外返回存储实例
  return { store, state };
}

使用场景

派生计算值

function ProgressBar() {
  const progress = useSelector(
    store.subscribe,
    () => store.getState(),
    (state) => state.duration > 0 
      ? (state.currentTime / state.duration) * 100 
      : 0
  );

  return <div className="progress" style={{ width: `${progress}%` }} />;
}

订阅多个状态片段

function MediaInfo() {
  const { paused, muted, volume } = useSelector(
    store.subscribe,
    () => store.getState(),
    (state) => ({
      paused: state.paused,
      muted: state.muted,
      volume: state.volume,
    }),
    shallowEqual
  );

  return (
    <div>
      {paused ? 'Paused' : 'Playing'}
      {muted ? ' (Muted)' : ''}
      Volume: {(volume * 100).toFixed(0)}%
    </div>
  );
}

参见