
Research
/Security News
Critical Vulnerability in NestJS Devtools: Localhost RCE via Sandbox Escape
A flawed sandbox in @nestjs/devtools-integration lets attackers run code on your machine via CSRF, leading to full Remote Code Execution (RCE).
@exodus/redux-dependency-injection
Advanced tools
Dependency-injection container for redux modules
Dependency injection framework on top of redux.
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".
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.
// 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
wand
and knownSpells
dependencies in tests.basicSpells
from the hogwarts
module, without importing them.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' }],
}
]
}
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 },
],
}
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.
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')
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)
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' }
],
}
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' }))
}
}
{
...,
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'],
}
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
}
}
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 }),
}
}
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')
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.
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.
FAQs
Dependency-injection container for redux modules
The npm package @exodus/redux-dependency-injection receives a total of 3,200 weekly downloads. As such, @exodus/redux-dependency-injection popularity was classified as popular.
We found that @exodus/redux-dependency-injection demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
/Security News
A flawed sandbox in @nestjs/devtools-integration lets attackers run code on your machine via CSRF, leading to full Remote Code Execution (RCE).
Product
Customize license detection with Socket’s new license overlays: gain control, reduce noise, and handle edge cases with precision.
Product
Socket now supports Rust and Cargo, offering package search for all users and experimental SBOM generation for enterprise projects.