npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@exodus/redux-dependency-injection

v4.1.1

Published

Dependency-injection container for redux modules

Downloads

16,622

Readme

@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',
  ]
}

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 { memoize } from 'lodash'
import { createSelector } from 'reselect'

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.