Security News
Supply Chain Attack Detected in Solana's web3.js Library
A supply chain attack has been detected in versions 1.95.6 and 1.95.7 of the popular @solana/web3.js library.
redux-scope
Advanced tools
Reduce Redux boilerplate and improve modularity.
Redux Scope logically brings together actions, action types, reducers and selectors by associating it with a redux scope. It provides helpers for creating actions and selectors which take care of creating unique action types and keep selectors wired correctly as you refactor your modules.
npm install redux-scope
Most of the apps are written as a collection of modules where each module has its own redux logic. Let's say we are writing a module taking care of user preferences, called preferences
:
src/modules/preferences/redux.js
import { createScope, createAction, createSelector } from 'redux-scope';
export const setFontSize = createAction('set-font-size');
function reducer(state = { fontSize: 'small' }, action) {
switch (action.type) {
case setFontSize.type:
return {
...state,
fontSize: action.payload,
};
default:
return state;
}
}
export const fontSize = createSelector(state => state.fontSize);
export const preferencesScope = createScope('preferences', reducer, {
setFontSize,
fontSizeSelector,
});
src/app.js
import { createStore } from 'redux';
import thunk from 'redux-thunk';
import { createRootReducer } from 'redux-scope';
import {
preferencesScope,
setFontSize,
fontSize as fontSizeSelector,
} from 'src/modules/preferences';
const rootReducer = createRootReducer({
preferencesScope,
// other scopes go here
});
const store = createStore(rootReducer, applyMiddleware(thunk));
// use redux as usual
store.dispatch(setFontSize('big'));
const fontSize = fontSizeSelector(store.getState()); // 'big'
Usually, Redux lifecycle can be separated into several phases:
combineReducers
,But before that, in order for modules to cooperate well:
Redux Scope uses this separation between composing redux logic from different modules (Step 1) and actually using that logic (Step 3) to lazy evaluate action types and correct paths for selectors, so that you don't need to hardcode them inside modules.
Here is how Redux lifecycle looks with Redux Scope:
createScope
combineScopes
createRootReducer
, which also initializes all action types and selectors to correct valuesYou don't have to think about action types, they will be prefixed automatically. When you write selectors with createSelector
you only write how to select a property inside that module (state => state.fontSize
), Redux Scope takes care of the rest.
src/modules/user/redux.js
import { createThunk, createScope } from 'redux-scope';
import { fetchUserDataAsync } from './my-user-api';
export const fetchUser = createThunk(fetchUserDataAsync, 'fetch-user');
const initialState = {
data: null,
loading: false,
error: null,
};
function reducer(state = initialState, action) {
switch (action.type) {
case fetchUser.type.request:
return {
...state,
loading: true,
error: null,
};
case fetchUser.type.success:
return {
...state,
data: action.payload,
loading: false,
error: null,
};
case fetchUser.type.error:
return {
...state,
loading: false,
error: action.error,
};
default:
return state;
}
}
export const userProfileScope = createScope('user', reducer, { fetchUser });
Let's say fetchUserDataAsync
looks like this:
function fetchUserDataAsync(userId, extraParam) {
return UserApi.getUser(id, extraParam);
}
fetchUser
thunk will dispatch 2 actions: request
, and success
or error
, that will look like this:
dispatch(fetchUser('id-123', true));
const requestAction = {
type: 'user/fetch-user/request',
request: ['id-123', true],
};
const successAction = {
type: 'user/fetch-user/success',
request: ['id-123', true],
payload: {
/* result of api call */
},
};
const errorAction = {
type: 'user/fetch-user/error',
request: ['id-123', true],
error: {
/* caught error */
},
};
Redux scopes can be composed using combineScopes
, analogous to how reducers can be composed using combineReducers.
We have already created two modules/scopes named user
and preferences
. Now, let's create a third module named favorites
to illustrate how scope composition works.
src/modules/favorites/redux.js
import { createScope, createAction, createSelector } from 'redux-scope';
export const addToFavorites = createAction('add-to-favorites');
function reducer(state = [], action) {
switch(action.type) {
case addToFavorites.type:
return [...state, action.payload],
default:
return state;
}
}
export const favorites = createSelector(); // by default you select state => state
export const favoritesScope = createScope('favorites', reducer, {
addToFavorites,
favorites,
});
Say we want to combine together favorites
and preferences
to form a new module named user-stuff
:
src/modules/user-stuff/redux.js
import { combineScopes } from 'redux-scope';
import { preferencesScope } from 'src/modules/preferences';
import { favoritesScope } from 'src/modules/favorites';
export userStuffScope = combineScopes('user-stuff', { preferencesScope, favoritesScope })
Now, at the top level:
src/app.js
import { createStore } from 'redux';
import thunk from 'redux-thunk';
import { createRootReducer } from 'redux-scope';
import { userScope } from 'src/modules/user';
import { userStuffScope } from 'src/modules/user-stuff';
const rootReducer = createRootReducer({
userScope,
userStuffScope,
});
const store = createStore(rootReducer, applyMiddleware(thunk));
✨ Generated root reducer from the last example produces state with the following shape:
{
"user": {
"data": null,
"error": null,
"loading": false
},
"user-stuff": {
"preferences": {
"fontSize": "small"
},
"favorites": []
}
}
Composing scopes automatically prefixes action types with scope names of all the parent scopes. Say, if we dispatch following actions:
dispatch(addToFavorites({ userId: 'id-123' }));
dispatch(setFontSize('large'));
dispatch(fetchUser('id-123', true));
created actions would have automatically scoped action types:
'user-stuff/favorites/add-to-favorites'
'user-stuff/preferences/set-font-size'
'user/fetch-user/request'
'user/fetch-user/success'
'user/fetch-user/error'
✨ All selectors are wired automatically, they recieve root state and work everywhere:
loading(state); // false
fontSize(state); // 'small'
✨ No matter where you import and use your selectors, you always pass the root state, no need to pass the substate selector manually.
✨ The mechanism of nesting scopes using composeScopes
makes sure all selectors remain connected to the right part of the state 🔬
✨ Your modules do not need to know where their reducer will be mounted, thus you get enhanced modularity.
✨ You can use your selectors like any other selector, compose them or use them with reselect.
import { createScope } from 'redux-scope';
import { reducer } from 'src/modules/external-module';
export const externalScope = createScope('some-external-module', reducer);
Provide a path to the mounting point so that selectors can work correctly:
src/modules/module-that-uses-redux-scope/index.js
import { createRootReducer } from 'redux-scope';
import { userScope } from 'src/modules/user';
import { userStuffScope } from 'src/modules/user-stuff';
export const reducer = createRootReducer(
{
userScope,
userStuffScope,
},
state => state.pathToThisModule,
);
Now you can use exported reducer like any other reducer, compose it with combineReducers, and all actions and selectors inside your module will work correctly.
combineSelectors
is a convenience mapper that reduces boilerplate in writing mapStateToProps
:
import { combineSelectors } from 'redux-scope';
import { fontSize, favorites } from './selectors';
// before
const mapStateToProps = (state, ownProps) => {
fontSize: fontSize(state),
favorites: favorites(state),
}
// after
const mapStateToProps = combineSelectors({
fontSize,
favorites
});
No problem, createAction
, createThunk
and createSelectors
accept one more parameter, that defines scope path or state selector, so you can use them without Redux Scope:
just-helpers.js
import { createAction, createThunk, createSelectors } from 'redux-scope';
import { asyncCall } from './async-call';
const initialState = {
someData: null,
ids: [],
count: 1,
};
const action = createAction('some-action', 'my/module');
const thunk = createThunk(asyncCall, 'my/module');
const { someData, ids, count } = createSelectors(
initialState,
state => state.my.module,
);
FAQs
Reduce Redux boilerplate and improve modularity 🔭
The npm package redux-scope receives a total of 1 weekly downloads. As such, redux-scope popularity was classified as not popular.
We found that redux-scope demonstrated a not healthy version release cadence and project activity because the last version was released 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.
Security News
A supply chain attack has been detected in versions 1.95.6 and 1.95.7 of the popular @solana/web3.js library.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.