从0到1写一个简单的react状态管理库

428 阅读7分钟

状态管理是react中必备的一项利器,能够自己实现一个,可以加深理解,提升自身实力,相关代码地址

下面实现一个简洁的状态管理库,如图所示

yuque_diagram.jpg

该实现包括一个 createStore 函数用于创建 store 对象,以及 useStore 自定义钩子用于连接 React 组件到 store

import { useEffect,useState } from "react";

// State类型
type State<T> = T | ((pre: T) => T) ;

// Store对象类型
type Store<T> = {

  getState: () => T;

  setState: (state: State<T>) => void;

  subscribe: (listener: () => void) => () => void;

};

// createStore函数,用于创建store对象
export function createStore<T>(initialState: T): Store<T> {

  let state: T = initialState;
  let listeners: Array<() => void> = [];

  // 获取当前state
  function getState(): T {
    return state;
  }

  // 更新state
  function setState(newState: State<T | Function>) {

    // 如果新的state是一个函数,那么执行该函数并将执行结果作为新的state

    if (typeof newState === 'function') {

      state = (newState as (prev: T) => T)(getState());

    } else {

      state = newState;

    }

    // 触发所有的监听器

    listeners.forEach((listener) => listener());

  }

  // 订阅state的变化
  function subscribe(listener: () => void): () => void {

    listeners.push(listener);

    // 返回一个函数,用于取消订阅
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  }

  return { getState, setState, subscribe };
}

// 自定义钩子,用于连接React组件到store
export function useStore<T>(store: Store<T>): T {

  const [state, setState] = useState(store.getState());

  useEffect(() => {

    // 每当store发生变化时,更新组件状态
    return store.subscribe(() => {

      setState(store.getState());

    });

  }, [store]);
  
  return state;
}

用法


import { createStore, useStore } from "../store";

// 创建 store 对象
const counterStore = createStore({
  count: 0,
});

// 定义 actions
const actions = {
  increment: () => {
    counterStore.setState((prevState) => ({ count: prevState.count + 1 }));
  },
  decrement: () => {
    counterStore.setState((prevState) => ({ count: prevState.count - 1 }));
  },
};

// 使用 useStore 钩子连接组件到 store
export function Counter() {
  const { count } = useStore(counterStore);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={actions.increment}>+</button>
      <button onClick={actions.decrement}>-</button>
    </div>
  );
}


在这个示例中,我们首先使用 createStore 函数创建了一个名为 counterStorestore 对象,并传递了一个初始状态对象。接下来,我们定义了两个名为 incrementdecrementactions,它们分别用于将 count 属性加一和减一。

最后,我们使用 useStore 钩子将组件连接到 counterStore 对象,并获取当前的 count 状态。我们在组件中渲染了当前的 count 值,并添加了两个按钮,用于调用 incrementdecrement 方法。当我们点击这些按钮时,组件将更新并显示更新后的 count 值。

以下是一个更复杂的使用示例,展示如何在 Todo 应用程序中使用上述状态管理器实现状态管理。

首先,我们定义一个 todo 的状态类型:


type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

type TodoState = {
  todos: Todo[];
};

接下来,我们使用 createStore 函数创建一个名为 todoStorestore 对象,并传递初始状态对象 { todos: [] }


const todoStore = createStore<TodoState>({ todos: [] });

我们可以使用 setState 方法来添加、更新和删除 todo:


const addTodo = (text: string) => {
  const newTodo: Todo = {
    id: Date.now(),
    text,
    completed: false,
  };
  todoStore.setState((prevState) => ({
    todos: [...prevState.todos, newTodo],
  }));
};

const updateTodo = (id: number, updates: Partial<Todo>) => {
  todoStore.setState((prevState) => {
    const updatedTodos = prevState.todos.map((todo) =>
      todo.id === id ? { ...todo, ...updates } : todo
    );
    return {
      todos: updatedTodos,
    };
  });
};

const deleteTodo = (id: number) => {
  todoStore.setState((prevState) => ({
    todos: prevState.todos.filter((todo) => todo.id !== id),
  }));
};

我们还可以定义一个名为 useTodos 的自定义钩子,用于将 todoStore 与组件连接起来:

const useTodos = () => {
  const state = useStore(todoStore);
  const add = useCallback(addTodo, []);
  const update = useCallback(updateTodo, []);
  const remove = useCallback(deleteTodo, []);
  return { todos: state.todos, add, update, remove };
};

在组件中,我们可以使用 useTodos 钩子获取当前的 todos 数组以及 add、update 和 remove 方法,以添加、更新和删除 todo

function TodoList() {
  const { todos, add, update, remove } = useTodos();
  const [newTodoText, setNewTodoText] = useState("");

  const handleAddTodo = (e: React.FormEvent) => {
    e.preventDefault();
    if (newTodoText.trim()) {
      add(newTodoText);
      setNewTodoText("");
    }
  };

  return (
    <div>
      <h1>Todos</h1>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          placeholder="Add a todo"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={(e) => update(todo.id, { completed: e.target.checked })}
            />
            <span>{todo.text}</span>
            <button onClick={() => remove(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

进一步扩展

  1. 添加中间件支持:可以通过添加中间件来增强状态管理器的功能,例如记录状态变更、实现异步 action、执行副作用等。

  2. 支持更多的 store 对象:可以扩展状态管理器,以支持多个 store 对象,并允许使用者在不同的组件中选择不同的 store 对象。

  3. 支持更复杂的状态结构:可以扩展状态管理器,以支持更复杂的状态结构,例如树形结构或嵌套结构。这可能需要引入新的 API,例如获取子状态或递归设置状态。

  4. 添加状态持久化支持:可以添加状态持久化支持,以允许状态在不同会话或页面之间进行保存和恢复。

  5. 添加时间旅行支持:可以添加时间旅行支持,以允许用户回溯和前进到不同的状态版本,并实现撤销和重做操作。

  6. 添加调试支持:可以添加调试支持,以允许开发者查看状态变化并进行调试。例如,可以实现一个开发者工具,用于显示状态树、操作日志和当前状态的快照。

支持中间件

要扩展代码以支持中间件,需要对 createStore 函数进行简单修改。我们可以向 createStore 函数中添加一个参数middleware,用于传递中间件函数。中间件函数接收一个 store 对象作为参数,并返回一个新的 store 对象。下面是修改后的代码:

import { useEffect, useState } from "react";

// State类型
export type State<T> = T | ((pre: T) => T);

// Store对象类型
export type Store<T> = {
  getState: () => T;

  setState: (state: State<T>) => void;

  subscribe: (listener: () => void) => () => void;
};

// createStore函数,用于创建store对象
export function createStore<T>(
  initialState: T,
  middleware?: (store: Store<T>) => Store<T>
): Store<T> {
  let state: T = initialState;
  let listeners: Array<() => void> = [];

  // 获取当前state
  function getState(): T {
    return state;
  }

  // 更新state
  function setState(newState: State<T>) {
    // 如果新的state是一个函数,那么执行该函数并将执行结果作为新的state

    if (typeof newState === "function") {
      // TODO: remove as
      state = (newState as (prev: T) => T)(getState());
    } else {
      state = newState;
    }

    // 触发所有的监听器

    listeners.forEach((listener) => listener());
  }

  // 订阅state的变化
  function subscribe(listener: () => void): () => void {
    listeners.push(listener);

    // 返回一个函数,用于取消订阅
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  }

  // store对象
  let store: Store<T> = { getState, setState, subscribe };
  // 如果有中间件,应用中间件
  if (middleware) {
    store = middleware(store);
  }

  return store;
}

// 自定义钩子,用于连接React组件到store
export function useStore<T>(store: Store<T>): T {
  const [state, setState] = useState(store.getState());

  useEffect(() => {
    // 每当store发生变化时,更新组件状态
    return store.subscribe(() => {
      setState(store.getState());
    });
  }, [store]);

  return state;
}

中间件用例LogCounter,打印新旧状态

import { createStore, State, Store, useStore } from "../store";

// 定义一个中间件
function loggingMiddleware<T>(store: Store<T>): Store<T> {
  const { getState, setState, subscribe } = store;

  return {

    getState,

    setState: (newState: State<T>) => {

      console.log("Old state:", getState());

      setState(newState);

      console.log("New state:", getState());

    },

    subscribe,
    
  };
}

// 创建 store 对象
const counterStore = createStore(
  {
    count: 0,
  },
  loggingMiddleware
);

// 定义 actions
const actions = {
  increment: () => {
    counterStore.setState((prevState) => ({ count: prevState.count + 1 }));
  },
  decrement: () => {
    counterStore.setState((prevState) => ({ count: prevState.count - 1 }));
  },
};

// 使用 useStore 钩子连接组件到 store
export function LogCounter() {
  const { count } = useStore(counterStore);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={actions.increment}>+</button>
      <button onClick={actions.decrement}>-</button>
    </div>
  );
}

支持传入多个middlewares中间件实现,关键利用reduceRight方法

import { useEffect, useState } from "react";

// State类型
export type State<T> = T | ((pre: T) => T);

// Store对象类型
export type Store<T> = {
  getState: () => T;

  setState: (state: State<T>) => void;

  subscribe: (listener: () => void) => () => void;
};

// 中间件工具类型
export type MiddlewareNext<T> = (state: State<T>) => void;

type Middleware<T> = (next: MiddlewareNext<T>) => MiddlewareNext<T>;

// createStore函数,用于创建store对象
export function createStore<T>(
  initialState: T,
  middlewares: Middleware<T>[] = []
): Store<T> {
  let state: T = initialState;
  let listeners: Array<() => void> = [];

  // 获取当前state
  function getState(): T {
    return state;
  }

  // 更新state
  function setState(newState: State<T>) {
    // 如果新的state是一个函数,那么执行该函数并将执行结果作为新的state

    if (typeof newState === "function") {
      // TODO: remove as
      state = (newState as (prev: T) => T)(getState());
    } else {
      state = newState;
    }

    // 触发所有的监听器

    listeners.forEach((listener) => listener());
  }

  // 订阅state的变化
  function subscribe(listener: () => void): () => void {
    listeners.push(listener);

    // 返回一个函数,用于取消订阅
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  }

  // 组合中间件处理函数,得到更新后的 setState 函数
  const enhancedSetState = middlewares.reduceRight(
    (next, middleware) => middleware(next),
    setState
  );

  return { getState, setState: enhancedSetState, subscribe };
}

// 自定义钩子,用于连接React组件到store
export function useStore<T>(store: Store<T>): T {
  const [state, setState] = useState(store.getState());

  useEffect(() => {
    // 每当store发生变化时,更新组件状态
    return store.subscribe(() => {
      setState(store.getState());
    });
  }, [store]);

  return state;
}

多个中间件从左到右依次执行的例子 LogTodoList

import { useState } from "react";
import { useCallback } from "react";
import { createStore, MiddlewareNext, State, useStore } from "../store";

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

type TodoState = {
  todos: Todo[];
};

// 日志中间件
function loggerMiddleware<T>(next: MiddlewareNext<T>): MiddlewareNext<T> {
  return (state: State<T>) => {
    console.log("before update:", todoStore.getState());
    next(state);
    console.log("after update:", todoStore.getState());
  };
}

// 计数中间件
function countMiddleware<T>(next: MiddlewareNext<T>): MiddlewareNext<T> {
  let count = 0;
  return (state: State<T>) => {
    count++;
    console.log("update count:", count);
    next(state);
  };
}

const todoStore = createStore<TodoState>({ todos: [] }, [
  loggerMiddleware,
  countMiddleware,
]);

const addTodo = (text: string) => {
  const newTodo: Todo = {
    id: Date.now(),
    text,
    completed: false,
  };
  todoStore.setState((prevState) => ({
    todos: [...prevState.todos, newTodo],
  }));
};

const updateTodo = (id: number, updates: Partial<Todo>) => {
  todoStore.setState((prevState) => {
    const updatedTodos = prevState.todos.map((todo) =>
      todo.id === id ? { ...todo, ...updates } : todo
    );
    return {
      todos: updatedTodos,
    };
  });
};

const deleteTodo = (id: number) => {
  todoStore.setState((prevState) => ({
    todos: prevState.todos.filter((todo) => todo.id !== id),
  }));
};

const useTodos = () => {
  const state = useStore(todoStore);
  const add = useCallback(addTodo, []);
  const update = useCallback(updateTodo, []);
  const remove = useCallback(deleteTodo, []);
  return { todos: state.todos || [], add, update, remove };
};

export function LogTodoList() {
  const { todos, add, update, remove } = useTodos();
  const [newTodoText, setNewTodoText] = useState("");

  const handleAddTodo = (e: React.FormEvent) => {
    e.preventDefault();
    if (newTodoText.trim()) {
      add(newTodoText);
      setNewTodoText("");
    }
  };

  return (
    <div>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          placeholder="Add a todo"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={(e) => update(todo.id, { completed: e.target.checked })}
            />
            <span>{todo.text}</span>
            <button onClick={() => remove(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}