SHIMMER
使用 React 和 TypeScript 实现自定义 Store
前端|react
发布于2025-02-11 最近修改2025-02-11
32
0
Shimmer

使用 React 和 TypeScript 实现自定义 Store

在 React 中,useStateuseReducer 是常见的状态管理方式,但在大型应用中,我们需要一种可以跨组件共享状态的方案。本文将介绍如何利用 React 和 TypeScript 实现一个简单的自定义 Store,支持异步操作,并且可以在多个组件之间共享状态。

核心功能亮点

✅ 类型安全的 Action 定义
✅ 异步 Action 原生支持
✅ 上下文隔离设计
✅ 优雅的降级处理
✅ 智能类型推导

代码实现

1. 创建 Store

我们通过 createStore 函数来创建自定义的 Store。该函数接收三个参数:storeName(Store 名称)、initialState(初始状态)和 actions(定义 Store 操作的动作函数)。该函数返回两个组件/钩子:StoreProvideruseStore,分别用于提供和访问 Store。
tsx
复制代码
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 会在该组件中提供。
tsx
复制代码
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。
tsx
复制代码
const MyComponent = () => { const { count, increment } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={() => increment()}>Increment</button> </div> ); };

3. 异步操作

在我们的 Store 中,actions 可以返回一个异步操作。如果某个 action 返回了一个 PromiseStoreProvider 会等待该 Promise 完成后再更新状态。这使得我们可以在 Store 中处理异步操作,例如从服务器获取数据。
tsx
复制代码
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 的子树中。
tsx
复制代码
// 错误的使用方式: // 在与 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 来优化性能。对于复杂的状态管理需求,这种模式提供了一种灵活且类型安全的解决方案。
目录