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 🙏

© 2026 – Pkg Stats / Ryan Hefner

reduxoscope

v1.1.0

Published

Reuse reducers and actions

Downloads

6

Readme

Reduxoscope

Tools for adding (and removing) scope to actions, made to help you reuse actions and reducers in different slices of state. Contains various functions to make it useful in real world scenario.

Tested to work with redux-toolkit.

Coming soon: helpers for integration with redux-observable and react-redux.

Table of contents

How does it work?

Scope is a unique string literal, that helps to scope action. Scope is contained in action's type. Actions can have more than one scope and scopes are not hierarchical. Usually, when function takes scope as one of arguments, it can be either string or array of strings, indicating several scopes.

scopedAction(action, scope) takes an action and appends scope to action's type field.

scopedAction({type: 'SOME_TYPE'}, 'some_scope');
//{type: @some_scope/SOME_TYPE}

scopedAction({type: 'SOME_TYPE'}, ['foo', 'bar']);
//{type: @foo@bar/SOME_TYPE}

scopedReducer(reducer, scope) wraps reducer and only calls it if action has provided scope or on initial action, dispatched by redux.

//reducer is called if action's type contains 'some_scope'
scopedReducer(reducer, 'some_scope')
//reducer is called if action's type contains 'foo' or 'bar' scope
scopedReducer(reducer, ['foo', 'bar'])

Note @ and / symbols in action's type. Scoped action has:

  • @ at the beginning of type, marking first scope
  • at least one symbol long scope. Scope can be any string literal, but SCOPE SHOULD NOT CONTAIN @ or / SYMBOLS
  • @ before any consecutive scopes
  • / after last scope

Imposing such rules on scoped type shape allows us to implement useful functions, such as getScopes(action), hasScope(action, [scope]), removeScopes(action) etc. and not misinterpret non scoped types, containing @ or / symbols.

Examples of some action types:

  • @a@b/some@type/completed - ✔ scoped type. scope is ['a', 'b'], type is some@type/completed
  • @@INIT@@ - ❌ not scoped type
  • @@INIT/REDUX - ❌ not scoped type
  • @foo@/bar - ❌ not scoped type

Basic Example

Let's say, you have state and somewhere in it there is paging, that's being managed with pagingReducer and some pagingAction.

//state shape
state = {
    clients: {
        paging: {/*...*/}
    }
};

Suddenly, you need to add paging somewhere else! But if you just add another paging object and reducer, both of them will change after pagingAction is dispatched.

import {createReducer, configureStore} from '@reduxjs/toolkit';
import {combineReducers} from 'redux';

//reducer, created with createReducer helper from 'redux-toolkit'
const pagingReducer = createReducer(/*...*/);

//here we use pagingReducer in both clientReducer and productReducer.
// This leads to both paging objects affected with pagingAction
const clientReducer = combineReducers({
    paging: pagingReducer,
});

const productsReducer = combineReducers({
    paging: pagingReducer,
});

const rootReducer = combineReducers({
    clients: clientReducer,
    products: productsReducer,
});

const state = {
    clients: {
        paging: {/*...*/}
    },
    products: {
        paging: {/*...*/}
    },
};

const store = configureStore({reducer: rootReducer, preloadedState: state});

To avoid the issue and reuse already existing pagingReducer and pagingAction, we can use scopedReducer and scopedAction functions.

import {createReducer, configureStore} from '@reduxjs/toolkit';
import {combineReducers} from 'redux';
import {scopedReducer, scopedAction} from 'reduxoscope';

const clientsScope = 'foo';
const productsScope = 'bar';

const pagingReducer = createReducer(/*...*/);


const clientReducer = combineReducers({
    paging: scopedReducer(pagingReducer, clientsScope),
});

const productsReducer = combineReducers({
    paging: scopedReducer(pagingReducer, productsScope),
});

const rootReducer = combineReducers({
    clients: clientReducer,
    products: productsReducer,
});

const state = {
    clients: {
        paging: {/*...*/}
    },
    products: {
        paging: {/*...*/}
    },
};

const store = configureStore({reducer: rootReducer, preloadedState: state});

//now, to modify paging, we need to dispatch action with appropriate scope

//clients.paging is affected, products.paging is not
store.dispatch(scopedAction(pagingAction(), clientsScope));

//products.paging is affected, clients.paging is not
store.dispatch(scopedAction(pagingAction(), productsScope));

//neither products.paging nor clients.paging are affected
store.dispatch(pagingAction());

API Reference

scopedAction(action: AnyAction, scope: string | string[]): AnyAction

Takes action and scope(s) and returns new action with scopes, appended to type field.

Does not mutate original action.

Example:

scopedAction({type: 'SOME_TYPE'}, 'some_scope');
//{type: @some_scope/SOME_TYPE}

scopedAction({type: 'SOME_TYPE'}, ['foo', 'bar']);
//{type: @foo@bar/SOME_TYPE}

scopedReducer(reducer: Reducer, scope: string | string[]): Reducer

Takes reducer and scope(s) and returns new reducer, that only calls passed reducer if scoped action contains any of passed scopes and on initial action, dispatched by redux.

Action, passed down to provided reduced, is cleared from all scopes.

Example:

const reducer = (state = initialState, action) => {    
    switch (action.type) {
        case 'ADD': //implementation
        case 'REDUCE': //implementation
    }
}

const reducerWithScope = scopedReducer(reducer, 'foo');

//when dispatching these actions, reducer is called and changes state 
scopedAction({type: 'ADD'}, 'foo');
scopedAction({type: 'REDUCE'}, 'foo');
scopedAction({type: 'ADD'}, ['bar', 'foo']);
scopedAction({type: 'REDUCE'}, ['bar', 'foo']);
//when dispatching these actions, reducer is not called
{type: 'ADD'}
{type: 'REDUCE'}
scopedAction({type: 'ADD'}, 'bar');
scopedAction({type: 'REDUCE'}, 'bar');
scopedAction({type: 'ADD'}, ['bar', 'baz']);
scopedAction({type: 'REDUCE'}, ['bar', 'baz']); 


//also takes array of scopes
const reducerWithScope = scopedReducer(reducer, ['foo', 'bar']);
//when dispatching these actions, reducer is called and changes state 
scopedAction({type: 'ADD'}, 'foo');
scopedAction({type: 'REDUCE'}, 'foo');
scopedAction({type: 'ADD'}, 'bar');
scopedAction({type: 'REDUCE'}, 'bar');
scopedAction({type: 'ADD'}, ['foo', 'bar']);
scopedAction({type: 'REDUCE'}, ['foo', 'bar']);
scopedAction({type: 'ADD'}, ['foo', 'baz']);
scopedAction({type: 'REDUCE'}, ['foo', 'baz']);
scopedAction({type: 'ADD'}, ['bar', 'baz']);
scopedAction({type: 'REDUCE'}, ['bar', 'baz']);

Important Note

Due to how redux works, when action is dispatched, ALL REDUCERS ARE CALLED, no matter action's type. That means, if you have non scoped reducers, that do not test action's type against exact string, such reducers may modify state even when scoped action is passed.

We are using redux-toolkit in next example:

const matchedReducer = createReducer(initialState, builder => builder
    .addMatcher(
        (action) => action.type.endsWith('do'),
        (state, action) => {
            //this function is called, when any action with type, ending with 'do', is dispatched.
            //This includes scoped actions, such as scopedAction({type: 'add_todo'}, 'some_scope'),
            //because scope is added to the beginning of action's type.
        }
    )
);
//if you need matchedReducer to filter out scoped actions, you can change matcher as follows
const matchedReducer = createReducer(initialState, builder => builder
    .addMatcher(
        (action) => !hasScope(action) && action.type.endsWith('do'),
        (state, action) => {}
    )
);

//some more examples
const regularReducer = (state = initialState, action) => {
    //this function is called, when any action is dispatched
    switch (action.type) {
        case 'add_todo': {
            //but code here is not evaluated, when scoped action is dispatched
        }
    }
};

const caseReducer = createReducer(initialState, builder =>
    builder.addCase('add_todo', (state, action) => {
        //this function is not called, when scoped action is dispatched
    })
);

getScopes(action: AnyAction): string[] | undefined

Returns scopes if action has any, otherwise returns undefined.

Examples:

const action = {type: 'some_type/something@example'};
const singleScopeAction = scopedAction(action, 'foo');
const severalScopesAction = scopedAction(action, ['foo', 'bar', 'baz']);

getScopes(singleScopeAction);
//['foo']
getScopes(severalScopesAction);
//['foo', 'bar', 'baz']
getScopes(action);
//undefined

hasScope(action: AnyAction, scope?: string | string[]): boolean

Checks if action has any of provided scopes. If scope is not provided, checks if action has any scope.

Example:

const action = {type: 'some_type'};
const singleScopeAction = scopedAction(action, 'foo');
const severalScopesAction = scopedAction(action, ['foo', 'bar', 'baz']);

hasScope(action);
//false
hasScope(singleScopeAction, 'foo');
//true
hasScope(singleScopeAction, ['foo', 'qux', 'something', 'bar']);
//true
hasScope(singleScopeAction);
//true
hasScope(singleScopeAction, 'bar');
//false
hasScope(severalScopesAction, 'foo');
//true
hasScope(severalScopesAction, ['foo', 'qux', 'something', 'bar']);
//true
hasScope(severalScopesAction);
//true
hasScope(severalScopesAction, 'qux');
//false
hasScope(severalScopesAction, ['qux', 'something']);
//false

removeScopes(action: AnyAction): AnyAction

Returns action with type, cleared of scopes.

Does not mutate original action.

Examples:

const action = {type: 'some_type'};
const singleScopeAction = scopedAction(action, 'foo');

removeScopes(singleScopeAction);
//{type: 'some_type'}
removeScopes(action);
//{type: 'some_type'}

pluckScopes(action: AnyAction): {action: AnyAction, scopes?: string[]}

Clears action of scopes and returns object, containing action without scopes and all scopes it previously had.

Does not mutate original action.

Examples:

const action = {type: 'some_type'};
const singleScopeAction = scopedAction(action, 'foo');

pluckScopes(singleScopeAction);
//{action: {type: 'some_type'}, scopes: ['foo']}
removeScopes(action);
//{action: {type: 'some_type'}}

scopedActionCreator(actionCreator: ActionCreator, scope: string | string[])

Returns new action creator, that adds scope to created action.

scopedDispatch(dispatch: Dispatch, scope: string | string[])

Returns new dispatch function, that adds scope to dispatched actions.

scopedType(type: string, scope: string | string[])

Returns type with appended scope.

Most likely you will never need to use this function. Used internally, but exposed if you really need it.

scopedType('SOME_TYPE', 'some_scope');
// @some_scope/SOME_TYPE

scopedType('SOME_TYPE', ['foo', 'bar']);
// @foo@bar/SOME_TYPE