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

@manukpl/storobs

v0.0.2

Published

Store Observables: simple redux-like store in typescript using rxjs.

Readme

storobs

Store Observables: simple redux-like store in typescript using rxjs.

Installation

yarn add @manukpl/storobs
# or
npm install @manukpl/storobs

A peer dependency for RXJS is required with a version inferior to v7.5.0 for compatibility issues, mainly with the rxjs version currently shipped with angular.

yarn add [email protected]
# or
npm install [email protected]

Example

import { createPayloadAction, Reducer, Store } from '@manukpl/storobs';

const INITIAL_STATE = { isLoading: false };
const setIsLoading = createPayloadAction<'setIsLoading', boolean>('setIsLoading');

type LoadingState = typeof INITIAL_STATE;
type LoadingAction = ReturnType<typeof setIsLoading>;
type LoadingReducer = Reducer<LoadingState, LoadingAction>;

const loadingReducer: LoadingReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'setIsLoading':
      return { ...state, isLoading: action.payload };

    default:
      return state;
  }
};

const store = new Store({ reducer: loadingReducer });

store.select('isLoading').subscribe(console.info);
store.dispatch(setIsLoading(true));

Usage

The Store is the main object of the lib. It takes a mandatory reducer function and exposes the methods to read and write the internal state.

const store = new Store({ reducer: loadingReducer });

Selectors

To read the state and listen for changes, use the select() function with a property from the state with returns an observable.

const isLoading$ = store.select('isLoading');

The select() function takes as many keys as needed to combine different part of the code and is able to infer correctly the type for each member up until 10 different keys.

const selectedUser$ = this.store
  .select('users', 'selectedUser')
  .pipe(
    map(([users, userId]) => {
      if (!userId) {
        return null;
      }
      return users.find((user) => user.id === userId) ?? null;
    }),
  );

Actions

To update the state, use the dispatch() function with an object matchin the Action type.

store.dispatch({ type: 'startLoading' });

Types and creator functions are available to create valid actions and simplify the syntax.

import { createEmptyAction, createPayloadAction } from '@manukpl/storobs';

const startLoading = createEmptyAction('startLoading');
const setIsLoading = createPayloadAction('setIsLoading');

Payload action require explicit type values to strictly type the payload

import { createEmptyAction, createPayloadAction, PayloadAction } from '@manukpl/storobs';

const setIsLoading = createPayloadAction('setIsLoading');
setIsLoading(true); // OK
setIsLoading(null); // OK

const setIsLoading = createPayloadAction<'setIsLoading', boolean>('setIsLoading');
setIsLoading(true); // OK
setIsLoading(null); // Type Error

const setIsLoading: PayloadAction<'setIsLoading', boolean> = createPayloadAction('setIsLoading');
setIsLoading(true); // OK
setIsLoading(null); // Type Error

Middlewares

Middlewares can be provided to the store on setup or dynamically added after its creation.

import { createEmptyAction, createPayloadAction, Middleware, Store } from '@manukpl/storobs';
import { tap } from 'rxjs/operators';

const startLoading = createEmptyAction('startLoading');
const setIsLoading = createPayloadAction<'setIsLoading', boolean>('setIsLoading');
type LoadingAction = ReturnType<typeof startLoading> | ReturnType<typeof setIsLoading>;

const loadingMiddleware: Middleware<LoadingState, LoadingAction> = (store) => (actions$) => {
  return actions$.pipe(
    tap({
      next: (action) => {
        if (action.type === 'startLoading') {
          store.dispatch(setIsLoading(true));
        }
      },
    }),
  );
};

const store = new Store({
  reducer: loadingReducer,
  middlewares: [loadingMiddleware],
});

// OR

const store = new Store({ reducer: loadingReducer });
store.addMiddleware(loadingMiddleware);

Middlewares should use the tap() rxjs operator to prevent interrupting the actions stream and having side effects on further middlewares or the reducer (unless that's the desired behaviour).

Async middleware

An asyncMiddleware is provided along with the createAsyncAction() helper to manage basic async effect in a similar way as thunks in redux.

import { asyncMiddleware, createAsyncAction, Reducer, Store } from '@manukpl/storobs';
import { of } from 'rxjs';

const INITIAL_STATE = { words: [] as string[], isLoading: false, error: null as unknown };
const fetchWords = createAsyncAction<'fetchWords', string[]>('fetchWords');

type WordsState = typeof INITIAL_STATE;
type WordsAction =
  | ReturnType<typeof fetchWords>
  | ReturnType<typeof fetchWords.pending>
  | ReturnType<typeof fetchWords.success>
  | ReturnType<typeof fetchWords.error>;

type WordsReducer = Reducer<WordsState, WordsAction>;
const reducer: WordsReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'fetchWords/pending':
      return { ...state, isLoading: true };

    case 'fetchWords/success':
      return { ...state, isLoading: false, words: action.payload, error: null };

    case 'fetchWords/error':
      return { ...state, isLoading: false, words: [], error: action.payload };

    default:
      return state;
  }
};

const store = new Store({
  reducer: reducer,
  middlewares: [asyncMiddleware],
});

const request$ = of(['hello', 'world']); // mock
store.dispatch(fetchWords(request$));

The action creator returned by createAsyncAction() takes an observable as its payload and exposes action creators for pending, success and error state, to use for typing or in other middlewares as failure recovery for instance.

Debug mode

A debug option can be passed to the store upon init to log each emitted action and state change (console.debug() might require verbose logging in a browser).

import { Store } from '@manukpl/storobs';
import { loadingReducer } from './reducer';

const store = new Store({
  reducer: loadingReducer,
  debugMode: true,
});

Full examples