使用 React 和 TypeScript 实现自定义 Store
在 React 中,useState
和 useReducer
是常见的状态管理方式,但在大型应用中,我们需要一种可以跨组件共享状态的方案。本文将介绍如何利用 React 和 TypeScript 实现一个简单的自定义 Store,支持异步操作,并且可以在多个组件之间共享状态。
核心功能亮点
✅ 类型安全的 Action 定义
✅ 异步 Action 原生支持
✅ 上下文隔离设计
✅ 优雅的降级处理
✅ 智能类型推导
代码实现
1. 创建 Store
我们通过 createStore
函数来创建自定义的 Store。该函数接收三个参数:storeName
(Store 名称)、initialState
(初始状态)和 actions
(定义 Store 操作的动作函数)。该函数返回两个组件/钩子:StoreProvider
和 useStore
,分别用于提供和访问 Store。
import React, { createContext, useContext, useReducer, useMemo } from 'react';
type AsyncActionFunction<T> = (
state: T,
...args: any[]
) => Promise<T> | T;
type StoreActions<T> = Record<string, AsyncActionFunction<T>>;
export function createStore<T extends object, A extends StoreActions<T>>(
storeName: string,
initialState: T,
actions: A
) {
// 修改ContextValue类型定义
type ContextValue = T & {
[K in keyof A]: (
...args: Parameters<A[K]> extends [T, ...infer Rest] ? Rest : never
) => ReturnType<A[K]> extends Promise<T> ? Promise<void> : void
};
// 定义自定义类型 StoreProviderProps
type StoreProviderProps = { children: React.ReactNode }
// 定义 Context
const Context = createContext<ContextValue | null>(null);
// 定义 reducer
function reducer(state: T, action: { type: keyof A; payload: any[] }): T {
const result = actions[action.type](state, ...action.payload);
return result instanceof Promise ? state : result;
}
// 定义 StoreProvider
function StoreProvider({ children }: StoreProviderProps) {
const [state, dispatch] = useReducer(reducer, initialState);
// 允许处理异步 Action
const boundActions = useMemo(() => {
return Object.keys(actions).reduce((acc, actionKey) => {
acc[actionKey as keyof A] = async (...args: any[]) => {
const result = actions[actionKey](state, ...args);
if (result instanceof Promise) {
const newState = await result;
dispatch({ type: actionKey, payload: [newState] });
} else {
dispatch({ type: actionKey, payload: args });
}
};
return acc;
}, {} as { [K in keyof A]: (...args: Parameters<A[K]>) => void });
}, [dispatch, state]);
const value = useMemo(
() => ({ ...state, ...boundActions }) as ContextValue,
[state, boundActions]
);
return <Context.Provider value={value}>{children}</Context.Provider>;
}
// 定义 useStore 钩子
function useStore(): ContextValue {
const context = useContext(Context);
if (!context) {
console.warn(`useStore must be used within ${storeName}Provider`);
// 创建类型安全的 fallback
const fallbackActions = (Object.keys(actions) as Array<keyof A>).reduce(
(acc, key) => {
const actionStub: ContextValue[keyof A] = ((...args: any[]) => { }) as any;
acc[key] = actionStub;
return acc;
},
{} as Pick<ContextValue, keyof A>
);
return {
...initialState,
...fallbackActions,
} as ContextValue;
}
return context;
}
return { StoreProvider, useStore };
}
2. 使用说明
StoreProvider
StoreProvider
组件提供了 Store 上下文,它应该包裹在需要访问状态的组件之上。通过 createStore
创建的 Store 会在该组件中提供。
const { StoreProvider, useStore } = createStore(
'myStore',
{ count: 0 },
{
increment: async (state) => {
return { ...state, count: state.count + 1 };
},
}
);
const App = () => (
<StoreProvider>
<MyComponent />
</StoreProvider>
);
useStore
useStore
是一个 hook,用来访问 Store 的状态和 actions。你可以在任何子组件中调用 useStore
来获取状态或执行 actions。
const MyComponent = () => {
const { count, increment } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => increment()}>Increment</button>
</div>
);
};
3. 异步操作
在我们的 Store 中,actions
可以返回一个异步操作。如果某个 action
返回了一个 Promise
,StoreProvider
会等待该 Promise
完成后再更新状态。这使得我们可以在 Store 中处理异步操作,例如从服务器获取数据。
const { StoreProvider, useStore } = createStore(
'myStore',
{ data: null },
{
fetchData: async (state) => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return { ...state, data };
},
}
);
const MyComponent = () => {
const { data, fetchData } = useStore();
useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
};
4. 使用时的注意事项
useStore
钩子只能在 StoreProvider
内部使用。如果在 StoreProvider
外部调用 useStore
,它将返回一个 fallback 值,并输出警告信息。
StoreProvider
必须在应用中所有需要访问 Store 的组件之上进行嵌套。否则,useStore
会无法获取到最新的 Store 状态。
5. 问题与优化
与同级组件的状态同步问题
需要注意的是,StoreProvider
提供的 useStore
只能获取到当前组件树下的最新状态。如果有组件与 StoreProvider
同级,它将无法访问最新的状态。因此,建议将 StoreProvider
包裹整个应用,或者将需要访问状态的组件移入 StoreProvider
的子树中。
// 错误的使用方式:
// 在与 StoreProvider 同级的组件中使用 useStore
const WrongComponent = () => {
const { count } = useStore(); // 这里的值可能不会是最新的
return <p>{count}</p>;
};
6. 核心架构图
[Action Dispatcher] --> [State Reducer]
↑ ↓
[Component] <-- [Context Provider]
总结
本文介绍了如何使用 React 和 TypeScript 实现一个自定义 Store,支持异步操作并能在多个组件之间共享状态。我们使用了 useReducer
来管理状态和 useContext
来提供共享的状态,且通过 useMemo
来优化性能。对于复杂的状态管理需求,这种模式提供了一种灵活且类型安全的解决方案。