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

redunk

v1.0.0

Published

Typed Redux reducers, action creators and hooks generated from a single declaration

Readme


Redunk lets you declare a slice of Redux state once and get everything else for free: the reducer, the action types, the action creators, and two fully typed hooks for reading state and dispatching actions.

No action type constants. No switch statements. No mapStateToProps. No manually typed selectors.

const { reducer, useActions, useReducerState } = createReducer({
  name: "counter",
  initialState: 0,
  reducers: state => ({
    increment: state + 1,
    decrement: state - 1,
    incrementBy: (amount: number) => state + amount,
  }),
});

That is the whole store slice. useActions() now returns { increment, decrement, incrementBy }, each correctly typed, and useReducerState() returns a number.

Features

  • Strong typing — state, action parameters and selectors are inferred by TypeScript. No generics to thread through your app.
  • Runtime typing — optional prop-types validation of every state transition, useful for plain JavaScript and untrusted API data.
  • Immutable by default — mutate the draft freely inside reducers; Immer produces the next state for you.
  • Self-contained reducers — components read and write a slice without knowing its name or where it sits in the store.
  • Multi-argument actions — a reducer case takes as many parameters as you like, not a single payload.
  • Drop-in compatible — works with plain Redux, combineReducers, connect(), useSelector/useDispatch and the Redux DevTools.

Installation

npm install redunk
yarn add redunk

Redunk relies on peer dependencies, so install Redux and React-Redux alongside it:

npm install redux react-redux

| Peer dependency | Supported range | | --------------- | ---------------------- | | react | ^18.0.0 \|\| ^19.0.0 | | react-redux | ^9.0.0 | | redux | ^5.0.0 |

Quick start

A complete counter application, from store to render.

import { createRoot } from "react-dom/client";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { createRootReducer } from "redunk";

// 1. Declare the state and how it changes.
const { reducer, useActions, useReducerState } = createRootReducer({
  initialState: 0, // State inferred as `number`
  reducers: state => ({
    increment: state + 1,
    decrement: state - 1,
  }),
});

// 2. Read and write it from any component.
function Counter() {
  const count = useReducerState();
  const { increment, decrement } = useActions();

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

// 3. Wire up the store as usual.
const store = createStore(reducer);

createRoot(document.getElementById("root")!).render(
  <Provider store={store}>
    <Counter />
  </Provider>
);

createRootReducer is for apps where a single reducer owns the entire store. Once you have more than one slice, use createReducer and give each slice a name.

Working with multiple reducers

createReducer namespaces its action types (counter/increment) and scopes useReducerState to its own slice. It also returns namedReducer — the reducer already wrapped in { [name]: reducer } — so combining slices is a matter of spreading them.

// store/counter.ts
import { createReducer } from "redunk";

export const { namedReducer, useActions, useReducerState } = createReducer({
  name: "counter",
  initialState: 0,
  reducers: state => ({
    increment: state + 1,
    decrement: state - 1,
  }),
});
// store/todos.ts
import { createReducer } from "redunk";

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

export const { namedReducer, useActions, useReducerState } = createReducer({
  name: "todos",
  initialState: [] as Todo[],
  reducers: state => ({
    add: (text: string) => {
      state.push({ text, completed: false });
    },
    toggle: (index: number) => {
      state[index].completed = !state[index].completed;
    },
    remove: (index: number) => {
      state.splice(index, 1);
    },
  }),
});
// store/index.ts
import { combineReducers, createStore } from "redux";
import { namedReducer as counter } from "./counter";
import { namedReducer as todos } from "./todos";

export const store = createStore(combineReducers({ ...counter, ...todos }));
// equivalent to combineReducers({ counter: counterReducer, todos: todosReducer })

Because the slice name travels with the reducer, a component never has to know where in the store its state lives:

import { useActions, useReducerState } from "./store/todos";

function TodoList() {
  const todos = useReducerState();
  const { toggle, remove } = useActions();

  return (
    <ul>
      {todos.map((todo, i) => (
        <li key={i}>
          <span onClick={() => toggle(i)}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </span>
          <button onClick={() => remove(i)}>x</button>
        </li>
      ))}
    </ul>
  );
}

Writing reducers

reducers is a function that receives the current state and returns an object of reducer cases. Each key becomes an action type and an action creator. A case can be one of three things:

createReducer({
  name: "example",
  initialState: { count: 0, label: "" },
  reducers: state => ({
    // 1. A plain value — the next state, dispatched with no arguments.
    reset: { count: 0, label: "" },

    // 2. A function returning the next state.
    setCount: (count: number) => ({ ...state, count }),

    // 3. A function that mutates the draft and returns nothing.
    //    Immer turns this into an immutable update.
    increment(by: number, reason?: string) {
      state.count += by;
      if (reason) state.label = reason;
    },
  }),
});

Cases that return void are handed to Immer, so mutating state is safe — a new object is produced instead of the original being modified.

Reducer case names become the action types that show up in the DevTools, so give them descriptive names. Unlike Redux Toolkit's createSlice, a case can declare any number of parameters:

const { useActions } = createReducer({
  name: "grid",
  initialState: [] as number[][],
  reducers: state => ({
    setCell: (row: number, column: number, value: number) => {
      state[row][column] = value;
    },
  }),
});

const { setCell } = useActions();
setCell(2, 5, 42); // fully typed

Hooks

Reducers created with Redunk expose two hooks.

useActions()

Returns an object of dispatch functions named after your reducer cases. It replaces useDispatch and mapDispatchToProps.

function Buttons() {
  const { increment, incrementBy } = useActions();

  return (
    <>
      <button onClick={increment}>+1</button>
      <button onClick={() => incrementBy(5)}>+5</button>
    </>
  );
}

useActions().incrementBy(5) is shorthand for dispatch({ type: "counter/incrementBy", payload: [5] }).

useReducerState(selector?, equalityFn?)

A wrapper around React-Redux's useSelector, already scoped to this reducer's slice. It replaces useSelector and mapStateToProps.

const wholeSlice = useReducerState();
const name = useReducerState(state => state.data.name);
const items = useReducerState(state => state.items, shallowEqual);

The component re-renders only when the selected value changes.

Using the classic Redux APIs

Redunk does not lock you in. The returned actions object holds ordinary action creators, so connect() and useDispatch work exactly as you would expect.

const { reducer, actions } = createReducer({
  name: "counter",
  initialState: 0,
  reducers: state => ({
    increment: state + 1,
    incrementBy: (amount: number) => state + amount,
  }),
});

actions.incrementBy(5);
// → { type: "counter/incrementBy", payload: [5] }
const mapDispatchToProps = dispatch => ({
  increment: () => dispatch(actions.increment()),
});

export default connect(state => ({ value: state.counter }), mapDispatchToProps)(Counter);

Action types are plain, readable strings, so they appear as-is in the Redux DevTools and can be handled by other reducers or middleware.

Runtime validation with prop-types

Pass proptypes to validate the state produced by initialState and by every action. This is especially handy in plain JavaScript projects, or when state is built from API responses that TypeScript cannot verify.

import PropTypes from "prop-types";

const { useActions } = createReducer({
  name: "user",
  initialState: { id: undefined, name: undefined, cookiesAccepted: false },
  proptypes: {
    id: PropTypes.string,
    name: PropTypes.string,
    cookiesAccepted: PropTypes.bool.isRequired,
  },
  reducers: state => ({
    setName: (name: string) => {
      state.name = name;
    },
  }),
});

proptypes accepts either an object of validators or a single validator for non-object state:

createRootReducer({
  initialState: 0,
  proptypes: PropTypes.number.isRequired,
  reducers: state => ({ increment: state + 1 }),
});

A failing transition logs a warning pointing at the exact action:

Warning: Failed value for type: Invalid value for `name` of type `number`
supplied to `user/setName`, expected `string`.

API

createReducer(options)

| Option | Type | Required | Description | | -------------- | --------------------------------- | -------- | ----------------------------------------------------------------------------------------- | | name | string | yes | Slice name. Used as the action type prefix (name/case) and as the key in namedReducer. | | initialState | any | yes | Initial state. Its type is inferred and drives the typing of everything else. | | reducers | (state) => Record<string, Case> | yes | Returns the reducer cases. Each key generates an action type and an action creator. | | proptypes | PropTypes validator or object | no | Validates state after initialState and after every action. |

Returns

| Key | Type | Description | | ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- | | namedReducer | object | { [name]: reducer }. Spread into combineReducers — the recommended way to mount a slice. | | useActions | function | Hook returning typed dispatch functions for every reducer case. | | useReducerState | function | Hook returning this slice's state; accepts an optional selector and equality function. | | actions | object | Plain action creators, e.g. { type: "todos/add", payload: [...] }, for use with connect() and useDispatch. | | reducer | function | The raw reducer function, for createStore() or a manual combineReducers entry. |

createRootReducer(options)

Identical to createReducer minus the name prefix. Action types are unprefixed and useReducerState reads the whole store state. Use it when one reducer owns the entire store.

Options: initialState, reducers, proptypes. Returns: reducer, actions, useActions, useReducerState.

Compared to Redux Toolkit's createSlice

Redunk covers the same ground as createSlice but adds the read/write hooks and lets cases take multiple arguments.

// Redux Toolkit
const value = useSelector(state => state.counter.value); // untyped without extra setup
const dispatch = useDispatch();
dispatch(incrementByAmount(5));

// Redunk
const value = useReducerState(); // typed, scoped to the slice
const { incrementByAmount } = useActions();
incrementByAmount(5, "because");

Since useReducerState knows its own slice, a component can consume a reducer without knowing its name, its state shape, or where it sits in the store — which makes reducers portable across projects.

TypeScript

Types are inferred end to end from initialState and the reducers function. You rarely need to annotate anything; when you do, annotate initialState:

type State = { items: string[]; loading: boolean };

createReducer({
  name: "list",
  initialState: { items: [], loading: false } as State,
  reducers: state => ({
    /* `state` is State, action parameters are yours to declare */
  }),
});

Module formats

The package ships both an ESM build (dist/esm) and a CommonJS build (dist/cjs), each with its own type declarations, selected through the exports map. It is marked sideEffects: false and is safe to tree-shake.

Documentation

Full documentation, guides and live CodeSandbox examples: ivoiv.github.io/redunk

Contributing

Issues and pull requests are welcome at github.com/ivoiv/redunk.

git clone https://github.com/ivoiv/redunk.git
cd redunk
npm install
npm test
npm run build

License

MIT © Ivo Ivanov