Skip to content

基于 React Hook 封装 Store 的三种方案

方案一:基于 useSyncExternalStore 的轻量级 Store(推荐)

tsx
import { useSyncExternalStore } from 'react';

type Store<T> = {
  state: T;
  listeners: Set<() => void>;
};

function createStore<T>(initialState: T) {
  const store: Store<T> = {
    state: initialState,
    listeners: new Set(),
  };

  const setState = (updater: (prev: T) => T) => {
    store.state = updater(store.state);
    store.listeners.forEach(listener => listener());
  };

  const subscribe = (listener: () => void) => {
    store.listeners.add(listener);
    return () => store.listeners.delete(listener);
  };

  const useStore = <S>(selector: (state: T) => S): S => {
    return useSyncExternalStore(
      subscribe,
      () => selector(store.state),
      () => selector(initialState)
    );
  };

  return [useStore, setState] as const;
}

特性:零依赖、无 Provider、精准渲染

方案二:基于 Context + useReducer

tsx
import { createContext, useContext, useReducer } from 'react';

type State = { count: number };
type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' };

const StoreContext = createContext<{
  state: State;
  dispatch: React.Dispatch<Action>;
} | null>(null);

function StoreProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer((prev: State, action: Action) => {
    switch (action.type) {
      case 'INCREMENT': return { count: prev.count + 1 };
      case 'DECREMENT': return { count: prev.count - 1 };
      default: return prev;
    }
  }, { count: 0 });

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('useStore must be used within StoreProvider');
  return context;
}

方案三:基于 useReducer + useContext

tsx
import { useReducer, useContext, createContext } from 'react';

const initialState = { count: 0 };

function reducer(state: typeof initialState, action: { type: string }) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

const CounterContext = createContext<{
  state: typeof initialState;
  dispatch: React.Dispatch<{ type: string }>;
} | null>(null);

export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}