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

redux-compact

v0.1.0-beta.0

Published

Action creator creator

Downloads

13

Readme

redux-compact

There comes a time in every Redux user's life, when they have to define an action type and write an action creator just to pass data to the corresponding reducer. redux-compact removes the boilerplate: you define reducers, and get action creators generated for you. Additionally:

  • if you use TypeScript, generated action creators will be fully typed - no redundant type definitions required;
  • action creators' structure will correspond to the structure of the state;
  • support for directing reducers towards particular item of the collection is out-of-the-box;
  • simple plugin system allows you to easily add the same reducer / action creator to multiple pieces of data;
  • redux-compact attempts at making as few assumptions as possible about what Redux middleware you're using.

The project is in early beta and cannot be considered stable. All feedback is welcome. So are feature requests.

Table of contents

Quick example: redux-compact vs vanilla Redux

Note: All examples are using TypeScript, as it allows taking full advantage of redux-compact - generated action creators are fully typed. However, if you don't use TypeScript in your project, you may still want to give redux-compact a try :)

redux-compact:

Imports

import { createStore, Store } from 'redux';
import { definition, list, create, combine, StateOf } from 'redux-compact';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
};

Definitions

const accessToken = definition<string | null>().setDefault(null).addReducers({
  loggedIn: (_state: string | null, accessToken: string) => accessToken,
  loggedOut: (_state: string | null) => null
});

const todo = definition<Todo>().addReducers({
  setCompleted: (todo: Todo) => ({...todo, completed: true})
});

const todos = list({ of: todo, key: todo => todo.id}).addReducers({
  add: (todos: Todo[], todo: Todo) => [...todos, todo]
});

const appState = combine({
  accessToken,
  todos
});

Usage

const { Actions, reduce } = create(appState);
type AppState = StateOf<typeof appState>;

const store: Store<AppState> = createStore(reduce);
store.dispatch(Actions.accessToken.loggedIn('access-token'));
store.dispatch(Actions.todos.add({id: "1", text: 'Add action types', completed: false}));
store.dispatch(Actions.todos.add({id: "2", text: 'Add action creators', completed: false}));
store.dispatch(Actions.todos.add({id: "3", text: 'Add reducers', completed: false}));
store.dispatch(Actions.todos.$item("1").setCompleted());

expect(store.getState()).toEqual({
  accessToken: 'access-token',
  todos: [
    {id: "1", text: "Add action types", completed: true},
    {id: "2", text: "Add action creators", completed: false},
    {id: "3", text: "Add reducers", completed: false},
  ]
});

Vanilla Redux

Imports

import { createStore, AnyAction, combineReducers, Store } from 'redux';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
};

Action types

const ACTION_LOGGED_IN = 'ACTION_LOGGED_IN';
const ACTION_LOGGED_OUT = 'ACTION_LOGGED_OUT';
const ACTION_TODO_ADD = 'ACTION_TODO_ADD';
const ACTION_TODO_COMPLETED = 'ACTION_TODO_COMPLETED';

Action creators

function loggedIn(accessToken: string) {
  return {
    type: ACTION_LOGGED_IN,
    accessToken
  };
};

function loggedOut() {
  return {
    type: ACTION_LOGGED_OUT,
  }
}

function addTodo(todo: Todo) {
  return {
    type: ACTION_TODO_ADD,
    todo
  }
}

function todoCompleted(todoID: string) {
  return {
    type: ACTION_TODO_COMPLETED,
    todoID
  }
}

Reducers

function accessToken(accessToken: string | null = null, action: AnyAction) {
  switch(action.type) {
    case ACTION_LOGGED_IN:
      return action.accessToken;
    case ACTION_LOGGED_OUT:
      return undefined;
    default:
      return accessToken;
  }
}

function todos(todos: Todo[] = [], action: AnyAction): Todo[] {
  switch(action.type) {
    case ACTION_TODO_ADD:
      return [...todos, action.todo];
    case ACTION_TODO_COMPLETED:
      const result = [...todos];
      const idx = result.findIndex(todo => todo.id == action.todoID);
      result[idx] = {...result[idx], completed: true};
      return result;
    default:
      return todos;
  }
}

const appState = combineReducers({accessToken, todos});

Usage

interface AppState {
  accessToken?: string | null;
  todo: Todo[];
};

const store: Store<AppState> = createStore(appState);
store.dispatch(loggedIn('access-token'));
store.dispatch(addTodo({id: "1", text: 'Add action types', completed: false}));
store.dispatch(addTodo({id: "2", text: 'Add action creators', completed: false}));
store.dispatch(addTodo({id: "3", text: 'Add reducers', completed: false}));
store.dispatch(todoCompleted("1"));

expect(store.getState()).toEqual({
  accessToken: 'access-token',
  todos: [
    {id: "1", text: "Add action types", completed: true},
    {id: "2", text: "Add action creators", completed: false},
    {id: "3", text: "Add reducers", completed: false},
  ]
});

Usage guide

Install

npm i --save redux-compact or yarn add redux-compact

Basic usage

Let's start simple and assume that your whole state is a counter. Start with importing relevant pieces from redux-compact:

import { definition } from 'redux-compact';

Let's first define our data type and the default value:

const counter = definition<number>().setDefault(0);

Now it's time to define the operations that you can perform on your piece of data:

const counter = definition<number>().setDefault(0).addReducers({
  incrementBy: (state: number, by: number): number => state + by,
  decrementBy: (state: number, by: number): number => state - by,
  reset: (_state: number) => 0
});

Important: Definitions are immutable, i.e. every call to setDefault, addReducers etc. creates a new one. TLDR DON'T do this:

const counter = definition<number>().setDefault(0);
counter.addReducers(...) // <-- this WILL NOT update counter

You can think of each value of the addReducers parameter as a single case in your regular-redux reducer. It should take the old state as a first argument, arbitrary number and types of other arguments, and return the new state following the usual redux immutability rule.

And... that's it. Let's create our store:

import { create } from 'redux-compact';
import { createStore } from 'redux';
const { Actions, reduce } = create(counter);
const store = createStore(reduce);

// And for TypeScript fans...
import { StateOf } from 'redux-compact'
type AppState = StateOf<typeof counter>; // == number

For each reducer we defined, corresponding action creator was generated: Let's see the store in action:

// Default value
expect(store.getState()).toEqual(0);

store.dispatch(Actions.incrementBy(3));
expect(store.getState()).toEqual(3);

store.dispatch(Actions.decrementBy(2));
expect(store.getState()).toEqual(1);

store.dispatch(Actions.reset());
expect(store.getState()).toEqual(0);

About default values

There are two cases where setDefault call is relevant (and necessary):

  • top-level definitions (inputs to create() function)
  • definitions combined in the combine() function (see below).

Composition

combine(definitions: {[key: string]: Definition})

Similarly to redux's combineReducers(), redux-compact provides combine() method:

import { combine } from 'redux-compact';

const appState = combine({
  counter1: counter,
  counter2: counter
});
type AppState = StateOf<typeof appState>; // { counter1: number, counter2: number }
const { Actions, reduce } = create(appState);
const store = createStore(reduce);

expect(store.getState()).toEqual({counter1: 0, counter2: 0});

store.dispatch(Actions.counter1.incrementBy(3));
expect(store.getState()).toEqual({counter1: 3, counter2: 0});

store.dispatch(Actions.counter2.incrementBy(5));
expect(store.getState()).toEqual({counter1: 3, counter2: 5});

Note how Actions structure mirrors the AppState structure.

definition.defineSlice(property, Definition)

With redux-compact, it's also possible to take a hybrid approach where you treat the object as a whole, but at the same delegate changes of its property to another definition, using defineSlice method:

type Player = {
  name: string;
  gamesPlayed: number
};

const player = definition<Player | null>().
  setDefault(null).
  addReducers({
    newPlayer: (_state: Player | null, name: string): Player => ({
      name,
      gamesPlayed: 0
    })
  }).
  defineSlice("gamesPlayed", counter);

const { reduce, Actions } = create(player);
const store = createStore(reduce);

expect(store.getState()).toEqual(null);

store.dispatch(Actions.newPlayer("Maciek"));
expect(store.getState()).toEqual({name: "Maciek", gamesPlayed: 0});

store.dispatch(Actions.gamesPlayed.incrementBy(1));
expect(store.getState()).toEqual({name: "Maciek", gamesPlayed: 1});

store.dispatch(Actions.newPlayer("Jagoda"));
expect(store.getState()).toEqual({name: "Jagoda", gamesPlayed: 0});

Note that gamesPlayed had to be defined in the Player type beforehand - the slice definitions add action creators for existing properties of the object. Similarly, the default value of the counter definition is irrelevant in this case.

Collections

list(config)

For colections, redux-compact provides special kind of definitions that allow addresing the particular item of the collection with a user-friendly API. For example, let's imagine we want to have a collection of Todo objects, uniquely defined by their ids:

interface Todo {
  id: string;
  text: string;
  completed: boolean;
};

First, let's define the definition for a single Todo object, like we previously did:

import { definition } from 'redux-compact';

const todo = definition<Todo>().addReducers({
  setCompleted: (todo: Todo) => ({...todo, completed: true})
});

What we can now do, is to create a list of such objects:

import { list } from 'redux-compact';

const todos = list({ of: todo, key: (todo: Todo) => todo.id});

import { StateOf } from 'redux-compact';
type Todos = StateOf<typeof todos>; // == Todo[]

The property of of the config is simply the definition for the item. key is a function that should return a string uniquely defining the Todo in the collection.

For completeness, let's add some collection-level reducer

const todos = list({ of: todo, key: (todo: Todo) => todo.id})
  .addReducers({
    add: (todos: Todo[], todo: Todo) => [...todos, todo]
  });

and see how we can use it:


const { Actions, reduce } = create(todos);
const store = createStore(reduce);

const todo1: Todo = { id: "1", text: "Test querying by key", completed: false };
const todo2: Todo = { id: "2", text: "Test querying by object", completed: false };

store.dispatch(Actions.add(todo1));
store.dispatch(Actions.add(todo2));
expect(store.getState()).toEqual([
  { id: "1", text: "Test querying by key", completed: false },
  { id: "2", text: "Test querying by object", completed: false }
]);

// We can select items by the key...
store.dispatch(Actions.$item("1").setCompleted());
expect(store.getState()).toEqual([
  { id: "1", text: "Test querying by key", completed: true },
  { id: "2", text: "Test querying by object", completed: false }
]);

// ...or by object (with the same key)
store.dispatch(Actions.$item(todo2).setCompleted());
expect(store.getState()).toEqual([
  { id: "1", text: "Test querying by key", completed: true },
  { id: "2", text: "Test querying by object", completed: true }
]);

nullableList(config, default: null | undefined)

If the state of the collection needs to allow null or undefined values, you can use nullableList() instead of list(). The second argument determines the state type for the definition:

import { nullableList } from '../..';

const todosOrNull = nullableList({of: todo, key: todo => todo.id}, null);
// StateOf<typeof todosOrNull> == Todo[] | null;

const todosOrUndefned = nullableList({of: todo, key: todo => todo.id}, undefined);
// StateOf<typeof todosOrNull> == Todo[] | undefined;

definition.use(listReducers)

redux-compact provides a plugin to add common reducers one might want to use with the list() definitions - no need to manually create the add() reducer like we did above.

import { listReducers } from '../../plugins';

const todos = list({of: todo, key: todo => todo.id}).use(listReducers);
const { Actions, reduce } = create(todos);
const store = createStore(reduce);

const todo1: Todo = { id: "1", text: "Test querying by key", completed: false };
const todo2: Todo = { id: "2", text: "Test querying by object", completed: false };

store.dispatch(Actions.pushOrReplace(todo1));
store.dispatch(Actions.pushOrReplace(todo2));
expect(store.getState()).toEqual([todo1, todo2]);

// todo1 is already in the list
store.dispatch(Actions.pushOrReplace(todo1));
expect(store.getState()).toEqual([todo1, todo2]);

// remove object
store.dispatch(Actions.remove(todo1));
expect(store.getState()).toEqual([todo2]);

// remove by key
store.dispatch(Actions.remove("2"));
expect(store.getState()).toEqual([]);

Plugins

The easiest way to reuse reducers / action creators between different pieces of data, is to create a plugin that can apply the changes to the arbitrary definition. It can be as simple as:

import { DefinitionBuilder } from 'redux-compact';

const setValueReducer = <State, ActionCreators, HasDefault>(
  definition: DefinitionBuilder<State,ActionCreators,HasDefault>
) => {
  return definition.addReducers({
    setValue: (_data: State, setValue: State) => setValue,
  });
}

and can be used with definition.use(plugin):

import { createStore } from 'redux';
import { create, definition } from 'redux-compact';

const counter = definition<number>().setDefault(0).use(setValueReducer);
const { Actions, reduce } = create(counter);
const store = createStore(reduce);

store.dispatch(Actions.setValue(7));
expect(store.getState()).toEqual(7);

Note that the setValueReducer is provided by 'redux-compact/plugins', along the others:

Plugin | Action creator | Description ------------ | ---------------|------------- setValueReducer | setValue(value: State) | Replaces the state with value objectReducers | update(values: Partial<State>) | Merges values into state listReducers | pushOrReplace(item: Item) | Appends the item to the state (which is a list). In case another item with the same key (see collections) is present, it is replaced by item.   | remove(item: Item \| string) | Given either an item or its key, it is removed from state (which is a list).

Custom action creators

It is also possible to provide action creators that are not in 1:1 correspondence with reducers, using definition.addActionCreators(). See the example below:

const counter = definition<number>().setDefault(0).use(setValueReducer).addActionCreators({
  reset: function () {
    return this.setValue(0);
  }
});
const { Actions, reduce } = create(counter);
const store = createStore(reduce);

store.dispatch(Actions.setValue(7));
expect(store.getState()).toEqual(7);

store.dispatch(Actions.reset());
expect(store.getState()).toEqual(0);

Note that this given to the custom action creator is an instance of Actions generated from this definition. It can use the reducers that were previously defined:

Note: in order to use this like that, you need to use old-style function syntax (arrow functions prevent explicitly binding this)

$context

Custom action creators can also leverage the collections API described above. For example, calling Actions.todos.$item("3").completed.fetch() exposes the key of the selected todo to the custom fetch() action creator. See example below:

import { definition, list, combine, create } from '../..';
import { listReducers, setValueReducer } from '../../plugins';
import { createStore } from 'redux';

type Todo = {
  id: string;
  text: string;
  completed?: boolean;
};

const tasksCompletedDataSource: {[key: string]: boolean | undefined} = {};
const completed = definition<boolean | undefined>().use(setValueReducer).addActionCreators({
  fetch: function() {
     // IMPORTANT: property name 'todo' comes from the list definition below
    const todoID = this.$context.todo;
    return this.setValue(tasksCompletedDataSource[todoID]);
  }
});

const todo = definition<Todo>().defineSlice("completed", completed);
const todos = list({
  of: todo,
  key: todo => todo.id,
  // IMPORTANT: This is the context key that the reducers can use
  contextName: 'todo',
}).use(listReducers);

Use it like you would with the reducer-based action creators:

const appState = combine({todos});
const { Actions, reduce } = create(appState);
const store = createStore(reduce);

const task1 = {id: "1", text: "Test with key"};
const task2 = {id: "2", text: "Test with object"};

store.dispatch(Actions.todos.pushOrReplace(task1));
store.dispatch(Actions.todos.pushOrReplace(task2));

tasksCompletedDataSource["1"] = true;
tasksCompletedDataSource["2"] = false;

store.dispatch(Actions.todos.$item("1").completed.fetch());
expect(store.getState()).toEqual({
  todos: [
    {id: "1", text: "Test with key", completed: true},
    {id: "2", text: "Test with object"}
  ]
});

store.dispatch(Actions.todos.$item(task2).completed.fetch());
expect(store.getState()).toEqual({
  todos: [
    {id: "1", text: "Test with key", completed: true},
    {id: "2", text: "Test with object", completed: false}
  ]
});