类型安全的状态管理
构建类型安全的状态管理
概述
状态管理是前端应用的核心架构之一。通过 TypeScript 的泛型和条件类型,可以构建类型安全的状态管理方案,确保状态结构、更新操作和派生数据都有精确的类型约束。本文介绍如何从零构建一个类型安全的 Store,涵盖状态定义、更新操作、选择器、中间件和异步操作等核心概念。
基础概念
Store:状态容器,持有应用的状态数据,提供读取和更新接口。Store 的状态类型在创建时确定,所有操作都受类型约束。
setState:状态更新方法,支持直接传入部分状态对象或更新函数。更新操作会与当前状态浅合并。
选择器(Selector):从 Store 状态中派生计算值的函数。选择器可以缓存计算结果,避免不必要的重渲染。
订阅(Subscribe):监听状态变化的机制。当状态更新时,所有订阅者都会收到通知。
中间件(Middleware):拦截状态更新的函数,可以实现日志记录、持久化、异步操作等横切关注点。
快速上手
基础 Store
// 类型安全的 Store 接口
interface Store<S extends object> {
getState(): S;
setState(partial: Partial<S> | ((state: S) => Partial<S>)): void;
subscribe(listener: (state: S) => void): () => void;
}
// 创建 Store
function createStore<S extends object>(initialState: S): Store<S> {
let state = { ...initialState };
const listeners = new Set<(state: S) => void>();
return {
getState: () => state,
setState(partial) {
const update = typeof partial === 'function' ? partial(state) : partial;
state = { ...state, ...update };
listeners.forEach((fn) => fn(state));
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
// 使用
interface AppState {
count: number;
user: { name: string; age: number } | null;
theme: 'light' | 'dark';
}
const store = createStore<AppState>({
count: 0,
user: null,
theme: 'light',
});
// 读取状态
store.getState().count; // number
// 更新状态
store.setState({ count: 1 });
store.setState((state) => ({ count: state.count + 1 }));
// 订阅变化
const unsubscribe = store.subscribe((state) => {
console.log('状态变化:', state.count);
});
类型安全的选择器
// 选择器函数
function createSelector<S, R>(
store: Store<S>,
selector: (state: S) => R,
equalityFn?: (a: R, b: R) => boolean
): () => R {
let lastValue = selector(store.getState());
// 订阅状态变化
store.subscribe((state) => {
const newValue = selector(state);
if (equalityFn ? !equalityFn(lastValue, newValue) : lastValue !== newValue) {
lastValue = newValue;
}
});
return () => lastValue;
}
// 使用
const getCount = createSelector(store, (state) => state.count);
const getUserName = createSelector(store, (state) => state.user?.name ?? '未登录');
console.log(getCount()); // number
console.log(getUserName()); // string
详细用法
类型安全的 Action
// 定义 Action 类型
interface Action<T extends string, P = void> {
type: T;
payload: P;
}
// Action 创建器
function createAction<T extends string>(type: T): { type: T };
function createAction<T extends string, P>(type: T): { type: T; payload: P };
function createAction<T extends string, P>(type: T) {
return (payload?: P) => ({ type, payload });
}
// 定义应用的 Actions
const increment = createAction<'increment', void>('increment');
const setUser = createAction<'setUser', { name: string; age: number }>('setUser');
const setTheme = createAction<'setTheme', 'light' | 'dark'>('setTheme');
// Reducer
type AppAction =
| Action<'increment'>
| Action<'setUser', { name: string; age: number }>
| Action<'setTheme', 'light' | 'dark'>;
function reducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setUser':
return { ...state, user: action.payload };
case 'setTheme':
return { ...state, theme: action.payload };
}
}
异步操作
// 类型安全的异步操作
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function createAsyncState<T>(): AsyncState<T> {
return { data: null, loading: false, error: null };
}
// 异步 Action
interface AsyncActions<T> {
request: () => void;
success: (data: T) => void;
failure: (error: string) => void;
}
function createAsyncActions<T>(store: Store<{ async: AsyncState<T> }>): AsyncActions<T> {
return {
request: () =>
store.setState((state) => ({
async: { ...state.async, loading: true, error: null },
})),
success: (data) =>
store.setState((state) => ({
async: { data, loading: false, error: null },
})),
failure: (error) =>
store.setState((state) => ({
async: { ...state.async, loading: false, error },
})),
};
}
// 使用
async function fetchUser(id: string) {
const actions = createAsyncActions<User>(userStore);
actions.request();
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
actions.success(user);
} catch (error) {
actions.failure(error instanceof Error ? error.message : '未知错误');
}
}
中间件
// 中间件类型
type Middleware<S> = (state: S, update: Partial<S>, next: (update: Partial<S>) => void) => void;
// 日志中间件
function loggerMiddleware<S>(
state: S,
update: Partial<S>,
next: (update: Partial<S>) => void
): void {
console.log('状态更新前:', state);
console.log('更新内容:', update);
next(update);
console.log('状态更新后:', state);
}
// 持久化中间件
function persistMiddleware<S extends object>(key: string): Middleware<S> {
return (state, update, next) => {
next(update);
// 更新后保存到 localStorage
const newState = { ...state, ...update };
localStorage.setItem(key, JSON.stringify(newState));
};
}
// 增强版 Store
function createStoreWithMiddleware<S extends object>(
initialState: S,
middlewares: Middleware<S>[]
): Store<S> {
const baseStore = createStore(initialState);
return {
...baseStore,
setState(partial) {
const update = typeof partial === 'function' ? partial(baseStore.getState()) : partial;
// 执行中间件链
let index = 0;
const next = (u: Partial<S>) => {
if (index < middlewares.length) {
middlewares[index++](baseStore.getState(), u, next);
} else {
baseStore.setState(u);
}
};
next(update);
},
};
}
常见场景
表单状态管理
interface FormState<T> {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
isSubmitting: boolean;
}
function createFormStore<T extends Record<string, any>>(initialValues: T) {
const store = createStore<FormState<T>>({
values: initialValues,
errors: {},
touched: {},
isSubmitting: false,
});
return {
store,
setFieldValue<K extends keyof T>(key: K, value: T[K]) {
store.setState((state) => ({
values: { ...state.values, [key]: value },
touched: { ...state.touched, [key]: true },
}));
},
setFieldError<K extends keyof T>(key: K, error: string) {
store.setState((state) => ({
errors: { ...state.errors, [key]: error },
}));
},
reset() {
store.setState({
values: initialValues,
errors: {},
touched: {},
isSubmitting: false,
});
},
};
}
分片状态
// 将状态分片管理
interface Slice<S, A> {
state: S;
actions: A;
}
function createSlice<
S extends object,
A extends Record<string, (state: S, ...args: any[]) => Partial<S>>,
>(config: {
initialState: S;
actions: A;
}): Slice<
S,
{
[K in keyof A]: A[K] extends (state: S, ...args: infer P) => any ? (...args: P) => void : never;
}
> {
const store = createStore(config.initialState);
const actions = Object.fromEntries(
Object.entries(config.actions).map(([key, action]) => [
key,
(...args: any[]) => {
const update = action(store.getState(), ...args);
store.setState(update);
},
])
) as any;
return { state: store.getState(), actions };
}
// 使用
const counterSlice = createSlice({
initialState: { count: 0, step: 1 },
actions: {
increment: (state) => ({ count: state.count + state.step }),
decrement: (state) => ({ count: state.count - state.step }),
setStep: (state, step: number) => ({ step }),
},
});
counterSlice.actions.increment(); // count + step
counterSlice.actions.setStep(5); // step = 5
注意事项
- 不可变更新:始终使用展开运算符或 immutable 更新库创建新状态对象,不要直接修改状态。直接修改会导致订阅者无法检测到变化。
- 浅合并:setState 使用浅合并,嵌套对象需要手动展开。对于深层嵌套状态,考虑使用 immer 等库简化不可变更新。
- 选择器性能:选择器应避免在每次调用时创建新对象(如数组或对象字面量),否则会导致不必要的重渲染。使用浅比较函数优化。
- 循环更新:在订阅回调中调用 setState 可能导致无限循环。确保在订阅回调中有适当的条件判断。
进阶用法
时间旅行调试
// 支持时间旅行的 Store
function createHistoryStore<S extends object>(initialState: S) {
const history: S[] = [initialState];
let currentIndex = 0;
const store = createStore(initialState);
store.subscribe((state) => {
// 记录每次状态变化
history.splice(currentIndex + 1);
history.push({ ...state });
currentIndex = history.length - 1;
});
return {
...store,
undo() {
if (currentIndex > 0) {
currentIndex--;
store.setState(history[currentIndex]);
}
},
redo() {
if (currentIndex < history.length - 1) {
currentIndex++;
store.setState(history[currentIndex]);
}
},
canUndo: () => currentIndex > 0,
canRedo: () => currentIndex < history.length - 1,
};
}
计算属性
// 类型安全的计算属性
interface ComputedState<S, C extends Record<string, (state: S) => any>> {
getState(): S & { [K in keyof C]: ReturnType<C[K]> };
setState(partial: Partial<S> | ((state: S) => Partial<S>)): void;
subscribe(listener: (state: S & { [K in keyof C]: ReturnType<C[K]> }) => void): () => void;
}
function withComputed<S extends object, C extends Record<string, (state: S) => any>>(
store: Store<S>,
computed: C
): ComputedState<S, C> {
const getComputedState = () => {
const state = store.getState();
const computedValues = Object.fromEntries(
Object.entries(computed).map(([key, fn]) => [key, fn(state)])
);
return { ...state, ...computedValues };
};
return {
getState: getComputedState as any,
setState: store.setState.bind(store),
subscribe(listener) {
return store.subscribe(() => listener(getComputedState()));
},
};
}