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

@danmartens/rx-state

v0.15.0

Published

A reducer-based React state management library that uses RxJS for managing side-effects.

Downloads

110

Readme

RxState

A reducer-based React state management library that uses RxJS for managing side-effects.

Creating a Store

import { createStore } from '@danmartens/rx-state';

type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' };

const counterStore = createStore((state: number, action: Action) => {
  switch (action.type) {
    case 'DECREMENT':
      return state - 1;

    case 'INCREMENT':
      return state + 1;

    default:
      return state;
  }
})(0);

Using a Store

const Counter = () => {
  const [state, dispatch] = useStore(counterStore);

  const decrement = () => {
    dispatch({
      type: 'DECREMENT',
    });
  };

  const increment = () => {
    dispatch({
      type: 'INCREMENT',
    });
  };

  return (
    <div>
      <div>{state}</div>

      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
};

Passing in the Initial State

const counterStore = createStore((state: number, action: Action) => {
  switch (action.type) {
    case 'DECREMENT':
      return state - 1;

    case 'INCREMENT':
      return state + 1;

    default:
      return state;
  }
});

const Counter = ({ initialValue }: { initialValue: number }) => {
  const [state, dispatch] = useStoreFactory(counterStore, initialValue);

  /* ... */
};

Simplifying the Reducer

import { createReducer } from '@danmartens/rx-state';

type State = number;
type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' };

const reducer = createReducer<State, Action>({
  DECREMENT: (state) => state - 1,
  INCREMENT: (state) => state + 1,
});

Effects

import { createReducer, setIn } from '@danmartens/rx-state';

type BlogPost = {
  slug: string;
  title: string;
  content: string;
};

type State = {
  posts: Record<string, BlogPost>;
  postStatuses: Record<string, 'pending' | 'persisted' | 'error'>;
};

type CreatePostAction = { type: 'CREATE_POST'; data: BlogPost };

type CreatePostSuccessAction = {
  type: 'CREATE_POST_SUCCESS';
  data: { slug: string };
};

type CreatePostErrorAction = {
  type: 'CREATE_POST_ERROR';
  data: { slug: string };
};

type Action =
  | CreatePostAction
  | CreatePostSuccessAction
  | CreatePostErrorAction;

const reducer = createReducer<State, Action>({
  CREATE_POST: (state, action) => {
    state = setIn(state, ['posts', action.data.slug], action.data);
    state = setIn(state, ['postStatuses', action.data.slug], 'pending');

    return state;
  },
  CREATE_POST_SUCCESS: (state, action) =>
    setIn(state, ['postStatuses', action.data.slug], 'persisted'),
  CREATE_POST_ERROR: (state, action) =>
    setIn(state, ['postStatuses', action.data.slug], 'error'),
});

const persistPost = (action$) =>
  action$.pipe(
    ofType('CREATE_POST' as const),
    mergeMap((action) =>
      from(
        fetch('/api/posts', {
          method: 'POST',
          headers: {
            'content-type': 'application/json',
          },
          body: JSON.stringify(action.data),
        }),
      ).pipe(
        map(() => ({
          type: 'CREATE_POST_SUCCESS',
          data: { slug: action.data.slug },
        })),
        catchError((error) => {
          console.error(error);

          return of({
            type: 'CREATE_POST_ERROR',
            data: { slug: action.data.slug },
          });
        }),
      ),
    ),
  );

const postsStore = createStore(reducer, [persistPost])({
  posts: {},
  postStatuses: {},
});

postsStore.next({
  type: 'CREATE_POST',
  data: {
    slug: 'hello-world',
    title: 'Hello, world!',
    content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
  },
});

API

createDispatcher()

By default, creating a store also creates an observable for dispatching actions (via store.next()). Sometimes it's useful for the dispatcher (essentially just an RxJS Subject) to be external so that it can be subscribed to by multiple stores.

For example, it might be useful to have a global "notifications" store that other stores can dispatch to (via effects):

import { createDispatcher } from '@danmartens/rx-state';

type NotificationsState = { message: string }[];
type PostsState = Record<string, { title: string; body: string }>;

type Action =
  | ShowNotification
  | CreatePostAction
  | CreatePostSuccessAction
  | CreatePostErrorAction;

interface ShowNotificationAction {
  type: 'SHOW_NOTIFICATION';
  message: string;
}

interface CreatePostAction {
  type: 'CREATE_POST';
  title: string;
  body: string;
}

interface CreatePostSuccessAction {
  type: 'CREATE_POST_SUCCESS';
}

interface CreatePostErrorAction {
  type: 'CREATE_POST_ERROR';
}

const action$ = createDispatcher<Action>();

const notificationsStore = createStore<{ message: string }[], Action>(
  (state, action) => {
    // ...

    return state;
  },
  [],
  {
    action$,
  },
);

const postsStore = createStore<State, Action>(
  (state, action) => {
    // ...

    return state;
  },
  [
    (action$) => {
      return action$.pipe(
        ofType('CREATE_POST_SUCCESS' as const, 'CREATE_POST_ERROR' as const),
        map((action) => {
          switch (action.type) {
            case 'CREATE_POST_SUCCESS': {
              return {
                type: 'SHOW_NOTIFICATION',
                message: 'Post created successfully',
              };
            }

            case 'CREATE_POST_ERROR': {
              return {
                type: 'SHOW_NOTIFICATION',
                message: 'Failed to create post',
              };
            }
          }
        }),
      );
    },
  ],
  {
    action$,
  },
)({});

Now when a post is created or fails to be created, the postsStore will dispatch a SHOW_NOTIFICATION action that the notificationsStore will receive.

NOTE: When multiple stores share the same dispatcher, all dispatched actions will be received by all subscribed stores. It's important that you return the current state at the end of every reducer function unless you are explicitly handling all actions in each reducer.

createEffect()

TODO: Document this function

createReducer()

TODO: Document this function

createSelector()

Creates a memoized selector function with up to three inputs that are also memoized. The selector function (always the final argument) is only called when the inputs change.

This is useful for creating functions that derive values from store state. It's designed to be used with the useSelector() hook that is created via createStoreContext().

Because the selector function passed to the useSelector() hook is called on every render and it causes the containing component to re-render when its return value changes, it's important to memoize the selector function if it is deriving a value from the store state.

A selector function that is only used to extract a subset of the store state should not be memoized via this function. For example:

const Session = () => {
  const currentUser = useSelector((state: State) => state.currentUser);

  // ...
};

The selector above does not need to be memoized via createSelector(). In fact, passing it into createSelector() will only add unnecessary overhead since the result of the selector function is already "referentially stable" (i.e. it's not re-computed on each render).

However, if the selector function is deriving a value from the store state, the result of the selector function may never be referentially stable if the value is non-primitive. For example:

const ActiveUsers = () => {
  const activeUsers = useSelector((state: State) =>
    state.users.filter((user) => user.isActive),
  );

  // ...
};

The selector above will return a new array every time any part of the state changes. This will cause the ActiveUsers component to re-render every time the state changes, even if the array of users is the same as it was before the state change.

We can improve this using createSelector():

const getActiveUsers = createSelector(
  (state: State) => state.users,
  (users) => users.filter((user) => user.isActive),
);

const ActiveUsers = () => {
  const activeUsers = useSelector(getActiveUsers);

  // ...
};

Now, the ActiveUsers component will only re-render when the array of users actually changes.

createStore(reducer, effects?, action$?)

Returns a function that accepts an initialState and an optional dependencies object (for effects).

createStore<S, A, D>(reducer: (state: S, action: A) => state, effects: Effect<S, A, D>[], action$?: Observable<A>): (initialState: S, dependencies: D) => Store<S, A, D>`

reducer: (state: S, action: A) => S

The reducer function is responsible for updating state whenever an action is dispatched. Just like with Redux, the state must be immutable and the reducer function must be pure (repeatedly calling the reducer with the same state and action inputs should always produce the same output).

effects: Effect<S, A, D>[]

TODO: Document this argument

action$: Observable<A>

TODO: Document this argument

createStoreContext()

TODO: Document this function

mapActions()

TODO: Document this function

Immutability Helpers

filter(target, predicate)

filter([1, 2, 3, 4], (n) => n % 2 === 0);

// => [2, 4]

map(target, callback)

map([1, 2, 3], (n) => n * 2);

// => [2, 4, 6]

mapEntries(target, callback)

TODO: Document this function

merge(target, value)

merge({ a: 1, b: 2 }, { a: 42 });

// => { a: 42, b: 2 }

push(target, value)

push([1, 2], 3);

// => [1, 2, 3]

set(target, key, value)

set({ value: 42 }, 'value', 84);

// => { value: 84 }

setIn(target, ...keys, value)

setIn({ nested: { value: 42 } }, 'nested', 'value', 84);

// => { nested: { value: 84 } }

splice()

TODO: Document this function

updateIn(target, ...keys, updater)

updateIn({ nested: { value: 42 } }, 'nested', 'value', (value) => value * 2);

// => { nested: { value: 84 } }

union(target, value)

union([1, 2, 3], [2, 4]);

// => [1, 2, 3, 4]

Influences

RxState is heavily inspired by my experiences with the following libraries and it wouldn't be what it is without them.

Goals

  • State can only be updated by dispatching an action, which is passed to a reducer (like Redux)
  • State can be global or scoped to a specific piece of UI (like Recoil)
  • The initial state can be passed in where the store is being used (i.e. it can be derived from props or hooks)
  • Side-effects are handled via Observables which receive dispatched actions after the state has been updated (like Redux Observable)
  • Side-effects can be cancelled by dispatching actions
  • Side-effect Observables can have dependencies, which are passed in when the store is initialized (and therefore can be derived from props or hooks)
  • Redux-like boilerplate is limited as much as possible

Non-goals

  • This library is not intended to replace data fetching libraries like SWR, React Query, Apollo Client, URQL, etc. It does, however, compliment them quite well.