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

@corndev/store

v1.0.0-beta.4

Published

Redux-shaped state container with no dependencies — usable with or without Corn UI

Readme

@corndev/store

A Redux-shaped state container in ~1.7 KB min+gzip, with zero dependencies.

It is not a Corn UI component. It ships no CSS and no markup, it does not import @corndev/ui, and it runs on a page with no Corn UI on it at all. When Corn UI is there, bind connects the two without either side importing the other.

npm install @corndev/store

Sixty seconds

import { createStore, createSlice, bind } from '@corndev/store';

const counter = createSlice({
  name: 'counter',
  initialState: { n: 0 },
  reducers: {
    added: (state, action) => ({ n: state.n + (action.payload ?? 1) }),
    reset: () => ({ n: 0 }),
  },
});

const store = createStore(counter.reducer);

// Render it. `bind` is what a hook is on a React page: the one call that
// connects state to the thing showing it.
const output = document.querySelector('#count');
bind(store, value => (output.textContent = value), state => state.n);

document.querySelector('#add')
  .addEventListener('click', () => store.dispatch(counter.actions.added()));

That is the whole integration. No build step, no framework, no provider.


The store

createStore(reducer, preloadedState?, enhancer?) returns Redux's own contract:

| Method | What it does | |---|---| | getState() | The current state. Never mutate the result. | | dispatch(action) | Runs the action through the reducer, then notifies subscribers. Returns the action. | | subscribe(listener) | Registers a listener; returns its unsubscribe function. | | replaceReducer(next) | Swaps the reducer, for code splitting. |

Two rules the store enforces rather than documents:

  • Reducers must not dispatch. Re-entering dispatch from inside a reducer throws instead of corrupting the state it is midway through computing.
  • Actions are plain objects with a string type. Dispatching anything else throws — except when thunk is installed, which is the point of thunk.

A store dispatches INIT on creation so reducers can supply their defaults, and REPLACE after replaceReducer so a swapped-in reducer can fill its slice.

Preloaded state

const store = createStore(rootReducer, window.__STATE__);

Useful for server-rendered HTML, or for restoring from localStorage.


Slices

createSlice derives each action type from the slice name and the handler key, so the action-type constant — the thing Redux codebases keep in a separate file and then misspell — has nowhere to go wrong.

const todos = createSlice({
  name: 'todos',
  initialState: { items: [], filter: 'all' },
  reducers: {
    added:   (state, action) => ({ ...state, items: [...state.items, action.payload] }),
    removed: (state, action) => ({ ...state, items: state.items.filter(t => t.id !== action.payload) }),
    filtered:(state, action) => ({ ...state, filter: action.payload }),
  },
  // Types this slice does not own, keyed by the full string.
  extraReducers: {
    'auth/loggedOut': () => ({ items: [], filter: 'all' }),
  },
});

todos.actions.added.type;        // 'todos/added'
`${todos.actions.added}`;        // 'todos/added' — usable as an object key
todos.actions.added({ id: 1 });  // { type: 'todos/added', payload: { id: 1 } }
todos.getInitialState();         // { items: [], filter: 'all' }

There is no Immer. Handlers return new state; they do not mutate a draft. state.items.push(x) is a bug here exactly as it is in a plain reducer. That is deliberate — a state library that pulls a 15 KB dependency is not a replacement for the thing it replaces.

Composing slices

import { combineReducers } from '@corndev/store';

const store = createStore(combineReducers({
  todos: todos.reducer,
  auth: auth.reducer,
}));

store.getState(); // { todos: { … }, auth: { … } }

combineReducers returns the same root object by identity when no slice moved. That is load-bearing rather than an optimisation: it is what makes the reference check in bind and useSelector worth anything.


Middleware

import { createStore, applyMiddleware, thunk, createLogger } from '@corndev/store';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk({ api }), createLogger({ filter: a => a.type !== 'ticked' })),
);

Middleware runs left to right. applyMiddleware can be passed as the second argument when there is no preloaded state, exactly as in Redux.

Thunks

A dispatched function runs instead of reaching the reducer, and receives (dispatch, getState, extra) — where extra is whatever you passed to thunk(...). Whatever the thunk returns is what dispatch returns, so an async thunk can be awaited.

const loadTodos = () => async (dispatch, getState, api) => {
  dispatch(todos.actions.loading());
  try {
    dispatch(todos.actions.loaded(await api.get('/todos')));
  } catch (error) {
    dispatch(todos.actions.failed(error.message));
  }
};

await store.dispatch(loadTodos());

The dispatch a thunk receives re-enters the whole middleware chain, so a thunk dispatching another thunk works.

For server data specifically, reach for @corndev/query instead of hand-rolling loading flags in a slice. Loading, error, staleness, deduplication and retries are its whole job.

Writing your own

const crashReporter = ({ getState }) => next => action => {
  try {
    return next(action);
  } catch (error) {
    report(error, { action, state: getState() });
    throw error;
  }
};

Selectors

createSelector(...inputs, combiner) recomputes only when an input changes. Without it, any selector that builds an object returns a fresh reference every call, and everything subscribed through it updates on every dispatch.

import { createSelector } from '@corndev/store';

const visibleTodos = createSelector(
  state => state.todos.items,
  state => state.todos.filter,
  (items, filter) => (filter === 'all' ? items : items.filter(t => t.done === (filter === 'done'))),
);

visibleTodos(store.getState());
visibleTodos.recomputations(); // for tests
visibleTodos.clear();          // drop the memo

Inputs may also be passed as an array, and extra arguments are forwarded to every input — createSelector((state, id) => state.byId[id], todo => todo.title).


Connecting it to the DOM

bind(source, target, select?, options?)

const unbind = bind(store, target, select, { equals, immediate });
  • source — anything with { getState, subscribe }. This store, a @corndev/query observer, or a real Redux store.
  • target — a function, or anything with an update(options) method, which is what every Corn UI create* controller returns.
  • select — narrows the state. Defaults to identity.
  • equals — skip the push when this returns true. Defaults to Object.is.
  • immediate — push the current value once at bind time. Defaults to true.

It returns its unbind. Unbinding does not destroy the target — bind did not create it.

// A plain callback
bind(store, todos => renderList(todos), state => state.todos.items);

// A Corn UI controller — no import of @corndev/ui here, and no import of
// @corndev/store there. `update()` is the entire contract.
import { createBadge } from '@corndev/ui';

const badge = createBadge(document.querySelector('#count'));
bind(store, badge, state => ({ variant: state.todos.items.length ? 'primary' : 'default' }));

derive(source, select, options?)

Wraps a source in a narrower one that only notifies when its slice changes. The result satisfies the same contract, so it composes with bind, with useSelector, or with another derive.

const openCount = derive(store, state => state.todos.items.filter(t => !t.done).length);

openCount.getState();          // 3
bind(openCount, n => (title.textContent = `${n} open`));

React

npm install @corndev/store @corndev/store-react

See @corndev/store-react. The hooks are written against { getState, subscribe } rather than against createStore, so they bind a real Redux store too — which is what makes a partial migration possible.


Coming from Redux

| Redux / Redux Toolkit | Here | |---|---| | createStore, configureStore | createStore(reducer, preloadedState?, enhancer?) | | combineReducers | combineReducers | | createSlice | createSlice — but handlers return state, they do not mutate a draft | | createAction | createAction | | applyMiddleware | applyMiddleware | | redux-thunk | thunk(extra) | | redux-logger | createLogger({ logger, filter }) | | reselect's createSelector | createSelector | | react-redux's Provider / hooks | @corndev/store-react | | createAsyncThunk | not included — use @corndev/query for server state | | Immer drafts | not included — return new state | | DevTools extension | not included — createLogger, or subscribe and log | | bindActionCreators | not included — dispatch(actions.x()) is short enough |

Because the store contract is Redux's own, migration can be partial in either direction: point @corndev/store-react's hooks at your existing Redux store first, and swap the store itself later, or never.


Types

.d.ts files are generated from the source JSDoc at build time, so the published types cannot drift from the documentation. No @types package is needed.

Licence

MIT © Corndog Development LLC