从零实现一个轻量级 React 状态管理库

3 阅读1分钟

前言

在 React 应用开发中,状态管理一直是开发者面临的核心挑战之一。随着应用规模的增长,组件间的状态共享和同步变得日益复杂。虽然 Redux、MobX、Zustand 等成熟方案提供了完善的解决方案,但有时候我们需要的只是一个轻量级、易理解、无依赖的状态管理工具。

本文将带你从零实现一个轻量级的 React 状态管理库,我们将称之为 MiniStore。通过这个实践,你不仅能掌握状态管理的核心原理,还能深入理解 React Hooks 的工作机制。

设计目标

在开始编码之前,我们先明确 MiniStore 的设计目标:

  1. 极简 API:提供最简单直观的 API 设计
  2. TypeScript 支持:完整的类型推断
  3. 高性能:最小化不必要的重渲染
  4. 零依赖:不依赖任何第三方库
  5. React 18 兼容:支持并发特性

核心实现

1. 创建 Store 核心

首先,我们实现 Store 的核心逻辑。Store 需要能够存储状态、管理订阅者、通知更新:

// types.ts
export type Listener<T> = (state: T) => void;
export type SetStateAction<T> = T | ((prevState: T) => T);
export type Selector<T, R> = (state: T) => R;

// store.ts
export class Store<T> {
  private state: T;
  private listeners: Set<Listener<T>> = new Set();

  constructor(initialState: T) {
    this.state = initialState;
  }

  getState(): T {
    return this.state;
  }

  setState(updater: SetStateAction<T>): void {
    const nextState = typeof updater === 'function'
      ? (updater as (prev: T) => T)(this.state)
      : updater;

    if (nextState !== this.state) {
      this.state = nextState;
      this.notify();
    }
  }

  subscribe(listener: Listener<T>): () => void {
    this.listeners.add(listener);
    return () => {
      this.listeners.delete(listener);
    };
  }

  private notify(): void {
    this.listeners.forEach(listener => {
      listener(this.state);
    });
  }
}

2. 实现 React Hooks 集成

接下来,我们创建 React Hooks 来连接 Store 和组件:

// hooks.ts
import { useEffect, useSyncExternalStore, useMemo } from 'react';
import { Store, Selector } from './store';

// 创建自定义 Hook 来使用 Store
export function useStore<T>(store: Store<T>): T {
  const state = useSyncExternalStore(
    store.subscribe.bind(store),
    store.getState.bind(store)
  );
  
  return state;
}

// 支持选择器的 Hook,用于性能优化
export function useStoreSelector<T, R>(
  store: Store<T>,
  selector: Selector<T, R>
): R {
  const getSnapshot = useMemo(() => {
    let lastSnapshot: R | null = null;
    let lastState: T | null = null;
    
    return () => {
      const currentState = store.getState();
      
      // 如果状态没有变化,返回缓存的快照
      if (currentState === lastState && lastSnapshot !== null) {
        return lastSnapshot;
      }
      
      lastState = currentState;
      lastSnapshot = selector(currentState);
      return lastSnapshot;
    };
  }, [store, selector]);

  const subscribe = useMemo(() => {
    return store.subscribe.bind(store);
  }, [store]);

  return useSyncExternalStore(subscribe, getSnapshot);
}

// 创建 Store 的 Hook
export function createStoreHook<T>(store: Store<T>) {
  return () => useStore(store);
}

export function createStoreSelectorHook<T>(store: Store<T>) {
  return <R>(selector: Selector<T, R>) => 
    useStoreSelector(store, selector);
}

3. 实现 Provider 模式

为了支持多个 Store 和更好的依赖注入,我们实现 Provider 模式:

// context.tsx
import React, { createContext, useContext, useMemo } from 'react';
import { Store } from './store';

// 创建 Store Context
export function createStoreContext<T>(initialState: T) {
  const StoreContext = createContext<Store<T> | null>(null);

  const StoreProvider: React.FC<{
    children: React.ReactNode;
    initialState?: T;
  }> = ({ children, initialState: providerInitialState }) => {
    const store = useMemo(() => {
      return new Store(providerInitialState || initialState);
    }, []);

    return (
      <StoreContext.Provider value={store}>
        {children}
      </StoreContext.Provider>
    );
  };

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

  return { StoreProvider, useStoreContext };
}

4. 实现 Action 和 Reducer 模式

为了提供更结构化的状态更新方式,我们实现 Action 和 Reducer 支持:

// reducer.ts
export type Action = {
  type: string;
  payload?: any;
};

export type Reducer<T> = (state: T, action: Action) => T;

export class ReducerStore<T> extends Store<T> {
  private reducer: Reducer<T>;

  constructor(initialState: T, reducer: Reducer<T>) {
    super(initialState);
    this.reducer = reducer;
  }

  dispatch(action: Action): void {
    const nextState = this.reducer(this.getState(), action);
    this.setState(nextState);
  }
}

// 创建 Reducer Store 的 Hook
export function useReducerStore<T>(
  reducer: Reducer<T>,
  initialState: T
): [T, (action: Action) => void] {
  const store = useMemo(() => {
    return new ReducerStore(initialState, reducer);
  }, [reducer, initialState]);

  const state = useStore(store);
  const dispatch = useCallback(
    (action: Action) => store.dispatch(action),
    [store]
  );

  return [state, dispatch];
}

完整示例:Todo 应用

让我们通过一个完整的 Todo 应用来演示如何使用我们的 MiniStore

// todoStore.ts
import { createStoreContext } from './context';

export interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

const initialState: TodoState = {
  todos: [],
  filter: 'all',
};

export const { StoreProvider: TodoProvider, useStoreContext } = 
  createStoreContext(initialState);

// 自定义 Hook 封装业务逻辑
export function useTodoStore() {
  const store = useStoreContext();
  
  const addTodo = useCallback((text: string) => {
    store.setState(prev => ({
      ...prev,
      todos: [
        ...prev.todos,
        {
          id: Date.now().toString(),
          text,
          completed: false,
        },
      ],
    }));
  }, [store]);

  const toggleTodo = useCallback((id: string) => {
    store.setState(prev => ({
      ...prev,
      todos: prev.todos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      ),
    }));
  }, [store]);

  const setFilter = useCallback((filter: TodoState['filter']) => {
    store.setState(prev => ({
      ...prev,
      filter,
    }));
  }, [store]);

  const filteredTodos = useStoreSelector(store, state => {
    switch (state.filter) {
      case 'active':
        return state.todos.filter(todo => !todo.completed);
      case 'completed':
        return state.todos.filter(todo => todo.completed);
      default:
        return state.todos;
    }
  });

  return {
    todos: filteredTodos,
    filter: useStoreSelector(store, state => state.filter),
    addTodo,
    toggleTodo,
    setFilter,
  };
}
// TodoApp.tsx
import React from 'react';
import { TodoProvider, useTodoStore } from './todoStore';

const TodoList: React.FC = () => {
  const { todos, toggleTodo } = useTodoStore();

  return (
    <ul>
      {todos.map(todo => (
        <li
          key={todo.id}
          onClick={() => toggleTodo(todo.id)}
          style={{
            textDecoration: todo.completed ? 'line-through' : 'none',
            cursor: 'pointer',
          }}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
};

const AddTodo: React.FC = () => {
  const [text, setText] = React.useState('');
  const {