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

@radio-garden/rematch

v1.1.0

Published

A lightweight, type-safe state management library built on Redux. Inspired by [Rematch](https://rematchjs.org/), with a simplified API and enhanced TypeScript support.

Readme

@radio-garden/rematch

A lightweight, type-safe state management library built on Redux. Inspired by Rematch, with a simplified API and enhanced TypeScript support.

Differences from Rematch

  • No plugins - Selectors and other features are built-in, not separate packages
  • store.selector and store.select - Two ways to access selectors: with state argument (for useSelector) or bound to current state
  • Auto-generated property selectors - selector.model.propertyName is automatically created for each state property
  • store.watchSelector - Built-in reactive state watching, also available inside effects
  • store.createSelector - Built-in memoized selector creation using reselect
  • Selectors defined on models - Define selectors directly in the model, with access to createSelector and cross-model selectors
  • Effects receive more utilities - Effects factory receives { dispatch, getState, select, selector, watchSelector }
  • onStore hook - Set up reactive side effects when the store is ready

Installation

npm install @radio-garden/rematch redux reselect
# or
pnpm add @radio-garden/rematch redux reselect

Quick Start

import { createModel, createRematchStore, type Models } from '@radio-garden/rematch';

// Define your models interface
interface AppModels extends Models<AppModels> {
  counter: typeof counter;
}

// Create a model
const counter = createModel<{ count: number }, AppModels>()({
  state: { count: 0 },
  reducers: {
    increment(state) {
      return { count: state.count + 1 };
    },
    add(state, payload: number) {
      return { count: state.count + payload };
    }
  },
  effects: ({ dispatch }) => ({
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000));
      dispatch.counter.increment();
    }
  }),
  selectors: ({ selector }) => ({
    doubled: state => selector.counter.count(state) * 2
  })
});

// Create the store
const store = createRematchStore<AppModels>({
  name: 'MyApp',
  models: { counter }
});

// Use it
store.dispatch.counter.increment();
store.dispatch.counter.add(5);
await store.dispatch.counter.incrementAsync();

console.log(store.getState().counter.count); // 7
console.log(store.select.counter.doubled()); // 14

API

createRematchStore<TModels>(config)

Creates a new store instance.

const store = createRematchStore<AppModels>({
  name: 'MyApp',                    // Optional store name
  models: { counter, user },        // Your models
  redux: {                          // Optional Redux configuration
    middlewares: [],                // Custom Redux middlewares
    devtoolOptions: { disabled: false },
    devtoolCompose: composeWithDevTools  // For React Native
  }
});

createModel<TState, TModels>()(modelConfig)

Creates a typed model definition.

const user = createModel<{ name: string; age: number }, AppModels>()({
  state: { name: '', age: 0 },
  reducers: {
    setName(state, name: string) {
      return { ...state, name };
    },
    setAge(state, age: number) {
      return { ...state, age };
    }
  },
  effects: store => ({
    async fetchUser(userId: string) {
      const user = await api.getUser(userId);
      this.setName(user.name);
      this.setAge(user.age);
    }
  }),
  selectors: ({ selector, createSelector }) => ({
    // Simple selector
    isAdult: state => selector.user.age(state) >= 18,

    // Memoized selector using createSelector
    fullInfo: createSelector(create =>
      create(
        [selector.user.name, selector.user.age],
        (name, age) => `${name} (${age} years old)`
      )
    )
  })
});

Selectors can be defined as a plain object or a factory function:

// Plain object (when you don't need cross-model selectors)
selectors: {
  doubled(state): number {
    return state.counter.count * 2;
  }
}

// Factory function (when you need access to other selectors or createSelector)
selectors: ({ selector, createSelector }) => ({
  doubled(state): number {
    return selector.counter.count(state) * 2;
  }
})

The factory function receives:

  • selector - Access selectors from any model
  • createSelector - Create memoized selectors using reselect
  • getState - Get current root state (useful for deriving types)

Important: Always add explicit return type annotations to selectors to avoid recursive type definitions:

selectors: ({ selector }) => ({
  // Explicit return types prevent TypeScript circular reference errors
  tabState(state): TabState | undefined {
    const { tab, tabs } = state.browser;
    return tab ? tabs[tab] : undefined;
  },
  currentPage(state): Page | undefined {
    return selector.browser.tabState(state)?.currentPage;
  }
})

For complex memoized selectors, use a type helper to ensure proper typing:

// Define a type helper for selectors
type CreateSelector<TRootState, TReturn> = (state: TRootState) => TReturn;

const browser = createModel<BrowserState, AppModels>()({
  state: { /* ... */ },
  selectors: ({ selector, createSelector, getState }) => {
    // Derive RootState type from getState
    type RootState = ReturnType<typeof getState>;

    // Use the type helper for complex return types
    const pages: CreateSelector<RootState, {
      currentPage: Page;
      previousPage: Page | undefined;
    }> = createSelector(create =>
      create(
        [selector.browser.history, selector.browser.pageCache],
        (history, pageCache) => ({
          currentPage: pageCache[history.current],
          previousPage: history.previous ? pageCache[history.previous] : undefined
        })
      )
    );

    return { pages };
  }
});

Store Methods

store.dispatch

Dispatch reducers and effects:

store.dispatch.counter.increment();           // No payload
store.dispatch.counter.add(5);                // With payload
store.dispatch.counter.addWithMeta(5, { multiplier: 2 }); // With meta
await store.dispatch.user.fetchUser('123');   // Async effect

store.selector

Get selectors that require state as an argument. Useful for React hooks like useSelector or when you need to work with a specific state snapshot.

const state = store.getState();

// Get entire model state
store.selector.counter(state);        // { count: 5 }
store.selector.user(state);           // { name: 'John', age: 25 }

// Auto-generated property selectors
store.selector.counter.count(state);  // 5
store.selector.user.name(state);      // 'John'

// Custom selectors defined in the model
store.selector.counter.doubled(state); // 10
store.selector.user.isAdult(state);    // true

Use with React Redux:

import { useSelector } from 'react-redux';

function Counter() {
  const count = useSelector(store.selector.counter.count);
  const doubled = useSelector(store.selector.counter.doubled);
  return <div>{count} (doubled: {doubled})</div>;
}

store.select

Get bound selectors that automatically use current state. Unlike store.selector, these don't require passing state as an argument.

// Get entire model state
store.select.counter();        // { count: 5 }
store.select.user();           // { name: 'John', age: 25 }

// Auto-generated property selectors
store.select.counter.count();  // 5
store.select.user.name();      // 'John'
store.select.user.age();       // 25

// Custom selectors defined in the model
store.select.counter.doubled(); // 10
store.select.user.isAdult();    // true

This is useful when you need quick access to the current state without managing state references:

// Inside an effect or anywhere with store access
if (store.select.user.isAdult()) {
  console.log(`User ${store.select.user.name()} is an adult`);
}

store.createSelector

Create memoized selectors using reselect:

const selectTotal = store.createSelector(create =>
  create(
    [state => state.counter.count, state => state.user.age],
    (count, age) => count + age
  )
);

selectTotal(store.getState()); // Memoized result

store.watchSelector

Watch for state changes:

// Watch a single selector
const unsubscribe = store.watchSelector(
  state => state.counter.count,
  (newValue, oldValue) => {
    console.log(`Count changed from ${oldValue} to ${newValue}`);
  }
);

// Watch multiple selectors
store.watchSelector(
  [state => state.counter.count, state => state.user.age],
  ([newCount, newAge], [oldCount, oldAge]) => {
    console.log('Values changed');
  }
);

// Trigger immediately with current value
store.watchSelector(
  state => state.counter.count,
  (value) => console.log('Current count:', value),
  { initial: true }
);

// Stop watching
unsubscribe();

store.subscribe

Low-level Redux subscription:

const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.getState());
});

store.getState

Get the current state:

const state = store.getState();
// { counter: { count: 0 }, user: { name: '', age: 0 } }

React Native Integration

For React Native with Flipper or Reactotron, pass a custom devtoolCompose:

import { composeWithDevTools } from '@redux-devtools/extension';

const store = createRematchStore<AppModels>({
  models: { counter },
  redux: {
    devtoolCompose: composeWithDevTools
  }
});

To disable devtools:

const store = createRematchStore<AppModels>({
  models: { counter },
  redux: {
    devtoolOptions: { disabled: true }
  }
});

Effects

The effects factory receives the store object with these properties:

  • dispatch - Dispatch actions to any model
  • getState - Get current root state
  • select - Bound selectors (no state argument needed)
  • selector - Selectors requiring state argument
  • watchSelector - Watch for state changes

You can use the store directly or destructure what you need:

// Using store directly
effects: store => ({
  async syncAll() {
    await this.fetchData();
    store.dispatch.otherModel.refresh();
  }
})

// Destructuring
effects: ({ dispatch, select, watchSelector, selector }) => ({
  async initialize() {
    // Use select for quick state access
    if (select.user.isAdult()) {
      dispatch.features.enableAdultContent();
    }

    // Watch for state changes within effects
    watchSelector(
      [selector.user.name, selector.settings.theme],
      ([name, theme]) => {
        console.log('User or theme changed');
      },
      { initial: true }
    );
  }
})

Effect Parameters

Each effect receives the payload as first argument and rootState as second:

effects: ({ dispatch }) => ({
  async updateIfNeeded(threshold: number, rootState) {
    if (rootState.counter.count > threshold) {
      dispatch.counter.reset();
    }
  }
})

Effects with this Context

Inside effects, this is bound to the model's dispatch object:

effects: {
  async saveUser(userData: UserData) {
    const saved = await api.saveUser(userData);
    this.setName(saved.name);  // Calls user.setName reducer
    this.setAge(saved.age);    // Calls user.setAge reducer
  }
}

onStore Hook

The onStore hook is called after the store is fully created. Use it to set up reactive side effects that respond to state changes.

This approach avoids circular dependency issues that would occur if you tried to import the store directly into your model file - the store is passed to you instead.

const counter = createModel<{ count: number }, AppModels>()({
  state: { count: 0 },
  reducers: {
    increment: state => ({ count: state.count + 1 }),
  },
  onStore: ({ watchSelector, selector }) => {
    // React to count changes
    watchSelector(selector.counter.count, (newCount, oldCount) => {
      console.log(`Count changed: ${oldCount} → ${newCount}`);
      syncToServer(newCount);
    });
  }
});

The onStore callback receives the full store object, which you can destructure to get only what you need:

// Full store access
onStore: store => {
  store.watchSelector(/* ... */);
}

// Destructured
onStore: ({ watchSelector, selector, dispatch, select, getState }) => {
  // Set up watchers, dispatch initial actions, etc.
}

Common Use Cases

Syncing state to external services:

onStore: ({ watchSelector, selector }) => {
  watchSelector(selector.user.preferences, prefs => {
    localStorage.setItem('preferences', JSON.stringify(prefs));
  });
}

Analytics tracking:

onStore: ({ watchSelector, selector }) => {
  watchSelector(
    [selector.player.currentTrack, selector.player.isPlaying],
    ([track, isPlaying]) => {
      if (track && isPlaying) {
        analytics.trackPlay(track.id);
      }
    }
  );
}

React Integration

Wrap your app with the Redux Provider and use useSelector with store.selector:

import { Provider, useSelector } from 'react-redux';
import { store } from './store';

// App wrapper
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

// Component using selectors
function Counter() {
  const count = useSelector(store.selector.counter.count);
  const doubled = useSelector(store.selector.counter.doubled);
  const user = useSelector(store.selector.user);

  return (
    <div>
      <p>Count: {count} (doubled: {doubled})</p>
      <p>User: {user.name}</p>
      <button onClick={() => store.dispatch.counter.increment()}>
        Increment
      </button>
      <button onClick={() => store.dispatch.counter.incrementAsync()}>
        Increment Async
      </button>
    </div>
  );
}

TypeScript

The library is fully typed. Define your models interface for complete type inference:

interface AppModels extends Models<AppModels> {
  counter: typeof counter;
  user: typeof user;
}

// All dispatch, selector, and select calls are fully typed
store.dispatch.counter.add(5);        // payload must be number
store.select.user.name();             // returns string
store.selector.counter.doubled(state); // returns number

License

MIT