@exodus/redux-dependency-injection

中文文档
5.0.0发布于 2026/06/06MITTypeScriptES Module近 30 天下载 2.2万

Dependency-injection container for redux modules

原始使用文档
文档目录8 节
  1. Why
  2. Redux Module Definitions
  3. TypeScript helper APIs
  4. Selectors
  5. Actions
  6. Reducer
  7. Usage
  8. Troubleshooting

@exodus/redux-dependency-injection

Dependency injection framework on top of redux.

Why

Large redux applications have hundreds of actions, reducers, and especially selectors. Actions don't even pretend to support dependency injection, reducers are not supposed to have any dependencies and though selectors are designed with dependency injection in mind (createSelector(...dependencies, resultFunction)), their dependencies are typically require'd / import'ed, which loses many of the benefits of dependency injection.

Outside of redux land, we have @exodus/dependency-injection, an IOC container that allows us to define dependencies in a declarative way. It then resolves the dependency graph and instantiates all nodes. This makes it easy to inject fakes during testing, different platform adapters on different platforms and different implementations of the same interface as needed. But you're not here for propaganda...

To adapt this to redux, we need to first support declaring redux components (actions, reducers, selectors) in a way that enables dependency injection. Enter "Redux Module Definitions".

Redux Module Definitions

Here is an example redux module definition written by JK Rowling:

{
  id: 'harryPotter',
  type: 'redux-module',
  // the `initialState` is used to auto-generate a base set of selectors
  initialState: {
    wand: null,
    knownSpells: [],
  },
  // how events get to the redux module is covered further down
  eventReducers: {
    harryGoesToOlivanders: (state, payload) => ({ ...state, wand: payload }),
    harryLearnsSpell: (state, payload) => ({ ...state, knownSpells: [...state.knownSpells, payload] }),
  },
  selectorDefinitions: [
    // vanilla selector
    {
      id: 'knowsBasicSpells',
      resultFunction: (knownSpells, basicSpells) => basicSpells.every((spell) => knownSpells.includes(spell)),
      // the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
      // based on `initialState`
      dependencies: [
        { selector: 'knownSpells' },
        // NOTE: this selector is external to the module
        { module: 'hogwarts' selector: 'basicSpells' },
      ],
    },
    // selector factory
    {
      id: 'knowsSpell',
      selectorFactory: (knownSpellsSelector) => (spell) => createSelector(
        knownSpellsSelector,
        knownSpells =>knownSpells.includes(spell)
      ),
      // the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
      // based on `initialState`
      dependencies: [{ selector: 'knownSpells' }],
    },
  ],
  // ACTIONS ARE (SORT OF) SUPPORTED, BUT NOT RECOMMENDED!
  // all your business logic belongs in `features` and `modules`, NOT in redux actions.
  // support for these may be removed in the future
  actionsDefinition: ({ hogwarts, harryPotter }) => {
    return {
      helpRonWithHomework: (spell) => (dispatch, getState) => {
        if (harry.selectors.knowsSpell(getState())(spell)) {
          dispatch(hogwarts.actions.submitHomework({ author: 'Ron' }))
        }
      }
    }
  },
  eventToActionMappers: {
    harryForgetsSpell: (spell) => ({ type: 'HARRY_POTTER_FORGETS_SPELL', payload: spell }),
  },
  dependencies: [
    'hogwarts',
  ]
}

TypeScript helper APIs

For TypeScript code, prefer helper APIs over raw satisfies definitions:

import {
  defineReduxModule,
  defineSelectorFactory,
  defineSelectorResultFunction,
} from '@exodus/redux-dependency-injection'
import { createSelector } from 'reselect'

const selectorDefinitions = [
  defineSelectorResultFunction({
    id: 'loaded',
    resultFunction: (data: { loaded: boolean }) => data.loaded,
    dependencies: [{ selector: 'data' }],
  }),
  defineSelectorFactory({
    id: 'createById',
    selectorFactory: (dataSelector: (state: unknown) => { id: string }[]) => (id: string) =>
      createSelector(dataSelector, (items) => items.find((item) => item.id === id)),
    dependencies: [{ selector: 'data' }],
  }),
] as const

export default defineReduxModule({
  id: 'example',
  initialState: { data: [] as { id: string }[] },
  eventReducers: {},
  selectorDefinitions,
} as const)

Let's dive a little deeper.

Selectors

Before

// harry-potter/selectors/knows-basic-spells.js
import { createSelector } from 'reselect'

import basicSpellsSelector from '~/ui/state/hogwarts/selectors/basic-spells'
import harryPotterKnownSpells from './known-spells'

const knowsBasicSpellsSelector = createSelector(
  basicSpellsSelector, // e.g. state => state.hogwarts.basicSpells
  harryPotterKnownSpells, // e.g. state => state.harryPottern.knownSpells
  (basicSpells, knownSpells) => basicSpells.every((spell) => knownSpells.includes(spell))
)

export default knowsBasicSpellsSelector

After

  1. We don't export the selector itself, but a definition of the selector, so it can be instantiated lazily by the IOC container. This also enables us inject fake wand and knownSpells dependencies in tests.
  2. We can depend on external selectors, such as basicSpells from the hogwarts module, without importing them.
  3. We no longer access global state. All data is accessed via other selectors. The knownSpells selector declared in dependencies is auto-generated by this library from the initialState.
{
  ...,
  selectorDefinitions: [
    // vanilla selector
    {
      id: 'knowsBasicSpells',
      resultFunction: (knownSpells, basicSpells) => basicSpells.every((spell) => knownSpells.includes(spell)),
      // the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
      // based on `initialState`
      dependencies: [
        { selector: 'knownSpells' },
        // NOTE: this selector is external to the module
        { module: 'hogwarts' selector: 'basicSpells' },
      ],
    },
    // selector factory
    {
      id: 'knowsSpell',
      selectorFactory: (knownSpells) => (spell) => knownSpells.includes(spell),
      // the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
      // based on `initialState`
      dependencies: [{ selector: 'knownSpells' }],
    }
  ]
}

Accessing state

As mentioned before for each property in initialState a separate selector is auto-generated. You can use these selectors by including them as a dependency in your definitions.

In the following example, two state selectors would be generated, wand and knownSpells, each returning their current value in the state.

const initialState = {
  wand: null,
  knownSpells: [],
}

export default initialState

If you want access to the whole state slice of a redux module you can use the special selector MY_STATE. This is a string constant defined in redux-dependency-injection.

import { MY_STATE } from '@exodus/redux-dependency-injection'

const someSelectorDefinition = {
  // ...
  dependencies: [
    // ...
    { selector: MY_STATE },
  ],
}

Selector definition properties

id

required

Identifies this selector when referencing it as a dependency in other selectors. We also use naming conventions for selectors that are part of redux modules.

resultFunction

required when not using selectorFactory

The resultFunc as it is defined in reselect.

This function accepts the outputs of selectors defined in dependencies.

const definition = {
  id: 'knowsBasicSpells',
  resultFunction: (knownSpells, basicSpells) => basicSpells.every((spell) => knownSpells.includes(spell)),
  dependencies: [
    { selector: 'knownSpells' },
    { module: 'hogwarts' selector: 'basicSpells' },
  ],
}

Example usage in a client of the above definition that belongs to a harryPotter redux module:

import { selectors } from '#/flux'

selectors.harryPotter.knowsBasicSpells(state)('finite')
selectorFactory

required when not using resultFunction

This function accepts selectors, listed in dependencies, instead of their outputs and returns a selector factory function.

It's a more performant alternative to a vanilla selector that returns a function. See our Selectors Best Practices for more details.

import lodash from 'lodash'
import { createSelector } from 'reselect'

const { memoize } = lodash

const definition = {
  id: 'createKnowsCharm',
  selectorFactory: (knownCharmsSelector) =>
    // meomize on charm since it is frequently used
    memoize((charm) =>
      createSelector(knownCharmsSelector, (knownCharms) => knownCharms.includes(charm))
    ),
  dependencies: [{ selector: 'knownCharms' }],
}

Example usage in a client of the above definition that belongs to a harryPotter redux module:

import { selectors } from '#/flux'

selectors.harryPotter.createKnowsCharm('expelliarmus')(state)
dependencies

optional

Allows you to inject and use other selectors by providing a list of identifier objects. The entries will passed to resultFunction or selectorFactory based on their position.

Selectors that belong to the module itself can simply be referenced by providing the selector's id.

{
  //...
  dependencies: [
    // first arg of resultFunction/selectorFactory
    { selector: 'charmSpells' },
    // second arg of resultFunction/selectorFactory
    { selector: 'curseSpells' },
  ],
}

When referencing a selector from a different module, the module id is required in addition to the selector id.

{
  //...
  dependencies: [
    // another redux module `hogwarts` that has a `basicSpells` selector
    { module: 'hogwarts', selector: 'basicSpells' }
  ],
}

Actions

Before

import hogwartsActions from '../hogwarts'
import harryKnowsSpell from '#/harry-potter/selectors/knows-spell'

export const helpRonWithHomework = (spell) => (dispatch, getState) => {
  if (harryKnowsSpell(getState())(spell)) {
    dispatch(hogwartsActions.submitHomework({ author: 'Ron' }))
  }
}

After

  1. We define action creators, rather than instantiate them.
  2. All dependencies are injected rather than imported, making the action testable and avoiding static dependencies on actions/selectors from other modules.
{
  ...,
  actionsDefinition: ({ hogwarts, harryPotter }) => {
    return {
      helpRonWithHomework: (spell) => (dispatch, getState) => {
        if (harry.selectors.knowsSpell(getState())(spell)) {
          dispatch(hogwarts.actions.submitHomework({ author: 'Ron' }))
        }
      }
    }
  },
  // in case you need to map events to actions for some reason, e.g.
  // because some other consumer expects this action. These are mutually exclusive with `eventReducers`
  eventToActionMappers: {
    harryForgetsSpell: (spell) => ({ type: 'HARRY_POTTER_FORGETS_SPELL', payload: spell }),
  },
  dependencies: ['hogwarts'],
}

Reducer

Before

function harryPotterReducer(state, { type, payload }) {
  switch (payload.type) {
    case 'HARRY_POTTER_GOES_TO_OLIVANDERS':
      return { ...state, wand: payload.wand }
    case 'HARRY_POTTER_LEARNS_SPELL':
      return { ...state, knownSpells: [...state.knownSpells, payload.spell] }
    default:
      return state
  }
}

After

Reducers are declared as a mapping from event names to transform functions. This allows to skip the manual step of mapping of event names to redux action types. This is (a bit) opinionated, because Exodus wallets use events to bubble up date from the SDK to the UI.

{
  ...,
  eventReducers: {
    harryGoesToOlivanders: (state, payload) => ({ ...state, wand: payload }),
    harryLearnsSpell: (state, payload) => ({ ...state, knownSpells: [...state.knownSpells, payload] }),
  }
}

There may be cases where it is necessary to ingest data through regular actions as opposed to events originating from the SDK. This can for example be data that is only relevant to the UI. To support this, a redux module can also define actionReducers:

{
  ...,
  actionReducers: {
    USE_DIFFERENT_WAND: (state, payload) => ({ ...state, wand: payload }),
    POTTER_CASTS_SPELL: (state, payload) => ({ ...state, spell: 'lumos' ?? payload }),
  }
}

Usage

See a complete example in @exodus/wallet-accounts under the /redux folder. Here's a minimal example:

// flux/index.js
import { setupRedux } from '@exodus/redux-dependency-injection'
import basicSpellsSelector from '~/ui/state/hogwarts/selectors/basic-spells'

const dependencies = [
  // external selector, e.g. from a client repo
  {
    id: 'hogwarts.selectors.basicSpells',
    factory: () => basicSpellsSelector,
  },
  harryPotterReduxModule,
]

const {
  reducers: mergedReducers,
  initialState: mergedInitialState,
  selectors,
  handleEvent,
  ioc,
} = setupRedux({
  // legacy reducers
  reducers,
  initialState,
  dependencies,
  logger: console,
})

const store = createStore(mergedReducers, mergedInitialState, enhancers)

const mergedActions = setupActions({ ioc, actions: legacyActionCreators })
const actions = bindAllActionCreators(store, mergedActions)

export { store, actions, selectors }

// to handle an event:
handleEvent('harryLearnsSpell', 'lumos')
// somewhere else in the app
import { selectors } from '#/flux'

selectors.harryPotter.knowsSpell('lumos')

Troubleshooting

Selectors are undefined

This might be caused by a require cycle. If the tests don't report this, use a client's console, e.g. mobile, to easily find what is causing the cycle.

Removing the require cycle should make selectors available again.

Events are not emitted or processed

Make sure an emitter for the event exists, e.g. an observer in the module's lifecycle plugin.

Also ensure that the event name between the emitter and the event reducer in the redux module's definition are the same.

版本与发布记录

@exodus/redux-dependency-injection 共发布 24 个版本,首次发布于 2023/05/08;过去 12 个月发布了 5 个版本。

版本发布日期距今
5.0.0latest2026/06/065 个月前
4.5.02026/05/016 个月前
4.4.02026/04/286 个月前
4.3.02026/04/286 个月前
4.2.02026/04/147 个月前
4.1.22025/08/131.3 年前

关于 @exodus/redux-dependency-injection 的常见问题

@exodus/redux-dependency-injection 是什么?

Dependency-injection container for redux modules

如何安装 @exodus/redux-dependency-injection?

在项目目录运行 npm install @exodus/redux-dependency-injection,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:redux(^4.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@exodus/redux-dependency-injection 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@exodus/redux-dependency-injection 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@exodus/redux-dependency-injection 可以商用吗?

@exodus/redux-dependency-injection 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@exodus/redux-dependency-injection 的下载量有多少?

根据 npm 官方下载统计,@exodus/redux-dependency-injection 在 2026/10/14 – 2026/11/12 这 30 天内被下载 22,293 次,日均约 743 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@exodus/redux-dependency-injection 还在维护吗?

最新版本 5.0.0 发布于 2026/06/06(5 个月前);至今共发布 24 个版本,其中过去 12 个月发布了 5 个;首次发布于 2023/05/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。