0.0.2 • Published 2 years ago

@manukpl/storobs v0.0.2

Weekly downloads
-
License
-
Repository
github
Last release
2 years ago

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 rxjs@7.4.0
# or
npm install rxjs@7.4.0

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