模块化 Redux Toolkit 应用程序

Modularize a Redux Toolkit application

问题

是否可以将依赖于 redux 存储的不同切片的基于 RTK 的应用程序的功能分离到单独的节点包中?假设是这样,最好的方法是什么?

背景

我们有一个基于 Redux Toolkit 的大型且不断增长的应用程序。在可能的情况下,我们尝试将应用程序的各个部分分离到它们自己的节点包中。我们发现这样做有很多好处,包括:

对于横切事物,如日志记录、http 请求、路由等,这样做很容易。但我们想更进一步,模块化我们应用程序的“功能”。例如,让我们的应用程序的“地址簿”功能与“消息”功能存在于不同的模块中,它们都通过“应用程序”包组合在一起。

我们在这里看到的好处是我们在其他代码库中发现并在其他地方讨论过的好处。 (例如,here for iOS)。但是,简而言之:(1) 您可以查看和控制应用程序内的依赖关系。例如,您可以轻松查看“消息”功能是否依赖于“地址簿”功能,并明确决定如何通过导出的内容将一个功能公开给另一个功能; (2) 你可以通过简单地拥有一个只包含你想要测试的东西的“预览”包来构建应用程序的完全可测试的子部分,例如,你可以有一个只依赖于“联系”功能,用于构建和测试; (3) 无需编译(TS/babel)、pack/minify和单元测试每个部分,可以加速CI/CD倍; (4) 您可以利用各种分析工具来更详细地了解每个功能的开发情况。

很可能还有其他方法可以实现这些目标,有些人可能不同意这是一个很好的方法这一前提。这不是问题的重点,但我愿意接受它可能是最佳答案的可能性(例如,一些具有丰富 Redux 经验的人可能会解释为什么这是一个坏主意)。

问题

我们一直在努力想出一种使用 Redux Toolkit 来实现这一点的好方法。问题似乎归结为——是否有一种好的方法来模块化(通过单独的节点包)RTK 中使用的各种“切片”? (这可能适用于其他 Redux 实现,但我们在 RTK 方面投入了大量资金)。

有一个包可以导出 redux 存储将使用的各种项目,即切片状态、动作创建者、异步 thunk 和选择器。然后 RTK 将在更高级别的应用程序中很好地组合这些。换句话说,您可以轻松地拥有一个包含商店的“app”包,然后是一个导出“contacts”切片的“contacts”包,以及它的伴随操作、thunk、选择器等。

如果您还希望 使用 部分切片的组件和挂钩与切片位于同一包中,例如,在“联系人”包中,那么问题就来了.那些 components/hooks 需要访问 global dispatch 和 global useSelector hook 才能真正工作,但这仅存在在“app”组件中,即组合各种功能包的功能。

考虑的可能性

  1. 我们可以从“更高”级别的“app”包中导出全局调度和useSelector,但是我们的子组件现在依赖于更高级别的包。这意味着我们不能再构建由不同排列的子包组成的替代更高级别的包。

  2. 我们可以使用单独的商店。这已经 in the past regarding Redux and has been discouraged, although there is some suggestion 如果您正在尝试实现模块化,它可能没问题。这些讨论也有些老了。

问题(再次)

是否可以将依赖于 redux 存储的不同切片的基于 RTK 的应用程序的功能分离到单独的节点包中?假设是这样,最好的方法是什么?

虽然我主要感兴趣的是 if/how 这是否可以在 RTK 中完成,但我也对答案感兴趣——尤其是来自对大型应用程序有 RTK/redux 经验的人——至于这是否是个坏主意,以及采取了哪些其他方法来实现模块化的好处。

这个问题在其他情况下也出现过,最值得注意的是如何编写需要知道给定切片的状态附加到根状态对象的选择器函数。 Randy Coulman 在 2016 年就该主题发表了一系列精彩而富有洞察力的博客 post,并在 2018 年发表了一篇后续文章 post,涵盖了几个相关方面 - 请参阅 Solving Circular Dependencies in Modular Redux post 和之前的链接。

我的一般想法是,您需要让这些模块提供一些允许注入根 dispatch 或向模块询问其提供的部分的方法,然后在应用程序级别将它们连接在一起.我自己不必处理任何这些,但我同意这可能是由于架构方面而使用 Redux 的较弱方面之一。

对于一些相关的现有技术,您可能需要查看这些库:

可能也值得在 RTK“讨论”区域提出同样的问题,以便我们进一步讨论。

根据@markerikson 的建议,这是我想出的解决方案。基本思想涉及依赖注入模型,其中更高级别 'app' 包:

  • 导入 'feature' 包的切片
  • 用它组成一个商店
  • 调用一个 initialize 函数,该函数也从 'feature' 包中导出,该函数注入调度和状态(实际上是包装它们的钩子,但您可以采用任何一种方式)。

最后一部分是允许 'feature' 包与 'app' 包保持分离的原因。

'feature' 程序包也有一些类型,将根状态和应用程序调度接口定义为包含至少 本地状态和该功能的调度的类型。

这是关键代码,使用 create-react-app 中的 redux-typescript 模板作为起点,并将 counter 功能提取到单独的包中。代码在counterSlice模块

// RootStateInterface is defined as including at least this slice and any other slices that
// might be added by a calling package
type RootStateInterface = { counter: CounterState } & Record<string, any>;

// A version of AppThunk that uses the RootStateInterface just defined
type AppThunkInterface<ReturnType = void> = ThunkAction<
  ReturnType,
  RootStateInterface,
  unknown,
  Action<string>
>;

// A version of use selector that includes the RootStateInterface we just defined
export let useSliceSelector: TypedUseSelectorHook<RootStateInterface> =
  useSelector;

// This function would configure a "local" store if called, but currently it is
// not called, and is just used for type inference.
const configureLocalStore = () =>
  configureStore({
    reducer: { counter: counterSlice.reducer },
  });

// Infer the type of the dispatch that would be needed for a store that consisted of
// just this slice
type SliceDispatch = ReturnType<typeof configureLocalStore>["dispatch"];

// AppDispatchInterface is defined as including at least this slices "local" dispatch and
// the dispatch of any slices that might be added by the calling package.
type AppDispatchInterface = SliceDispatch & ThunkDispatch<any, any, any>;

export let useSliceDispatch = () => useDispatch<AppDispatchInterface>();

// Allows initializing of this package by a calling package with the "global"
// dispatch and selector hooks of that package, provided they satisfy this packages
// state and dispatch interfaces--which they will if the imported this package and
// used it to compose their store.
export const initializeSlicePackage = (
  useAppDispatch: typeof useSliceDispatch,
  useAppSelector: typeof useSliceSelector
) => {
  useSliceDispatch = useAppDispatch;
  useSliceSelector = useAppSelector;
};

rush repository.

中提供了此解决方案的工作示例