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 🙏

© 2024 – Pkg Stats / Ryan Hefner

epickit

v2.0.0

Published

State manager for your reactive application.

Downloads

5

Readme

epickit

CircleCI codecov

epickit is a state manager that utilises observable streams (rxjs) in order to facilitate state changes in context of async operations in your reactive application. It is meant to be used in your frontend, backend or any other type of applications (e.g. mobile apps, IoT-apps, edge agents, etc.).

Introduction

epickit requires a basic understanding of observable streams, (RxJS v6 to be more specific). This implementation and the API is very similar to redux-observable but without any dependency to redux 🎉.

API

createAction<State, Payload>(...args: CrateActionArgs) => (p?: Payload) => Action<State, Payload>

An action creator facilitates definition and filtering of actions. An action is an interface with the following definition:

interface IAction<State, Payload>{
  label: string;
  reducer: (s: State, p: payload) => s;
  payload: Payload;
}

Function overloading is utilisied in order to enable different signatures unter the same function name createAction. Basically we have three options to call createAction:

  • createAction([label: string]) with optional label for e.g. debugging and without any payload
  • createAction<State>([[label: string], reducer: Reducer<State>]) with reducer (read more about reducers in the next sections)
  • createAction<State, Payload>([[label: string], reducer: Reducer<State, Payload>]) with optional reducer but required payload

createAction is a generic function that can be called without any types defined. This is sufficient in situation where you want to create an action that neither mutates the state nor carries any payload. If you want to mutate the state then you have to provide a reducer to createAction. If you provide a reducer then it's also required that you define the interface of the state object on which the reducer is going to be called e.g.

// here our state is of type number
const inc = createAction<number>((counter) => counter + 1);

dispatch(inc());
// dispatch({label: "", reducer: (s) => s + 1, payload: undefined})

If you want to create an action with payload then you need to provide the second type parameter to createAction:

const add = createAction<number, number>((counter, n) => counter + n);

dispatch(add(5));
// dispatch({label: "", reducer: (s, n) => s + n, payload: 5})

If you want to create an action that doesn't mutate the state but contains some payload then you can leave the reducer undefined:

const sendNumber = createAction<any, number>();

dispatch(sendNumber(99));
// dispatch({label: "", reducer: (s) => s, payload: 99})

As you can see from the examples createAction doesn't directly return the expected action object when calling it. Instead it takes a intermediate step by returning another function that might take the payload as an argument depending on the type parameters you have provided to createAction<State, Payload>.

Reducer<State, Payload>

If comparing to the previous examples the following reducer use case is based on more realistic scenario with a state that is stored in a nested object:

interface IState {
  users: IUser[],
  counter: {
    errors: string[];
    value: number;
  }
}

const inc = createAction<IState>((s) => ({
  ...s,
  counter: {
    ...s.counter,
    value: s.counter + 1,
  }
}));

Updating a nested property can be quite inconvenient when working with such nested object structures because you have to explicitly copy all the other properties from the old to your new state. So as an alternative to the flat reducers presented so far this library also supports nested reducers:

const inc = createAction<IState>({
  counter: {
    value: (c) => c + 1,
  },
});

const addUser = createAction<IState, IUser>({
  users: (users, u) => [...users, u],
  counter: {
    value: (c) => c + 1,
  },
});

combineEpics<State>(...epic: Array<Epic<State>>): Epic<State>

Similar to redux-observable epickit also provides a utility that allows you to combine multiple epics into a single one.

import {fetchDevices, fetchLogs} from "./epics";

export const epics = combineEpics(
  fetchDevices,
  fetchLogs,
);

filterAction(a: ActionCreator<S, P>): OperatorFunction<IAction<S, any>, P>

filterAction is an operator function that can be composed within a RxJS pipe in order to filter an observable stream of actions by an action creator.

const add = createAction<number, number>();
const inc = createAction<number>((c) => c + 1);

const addToCounterEpic: Epic<number> = (action$) =>
  action$.pipe(
    filterAction(add),
    mergeMap((n) =>
      from(Array(n).fill(undefined)),
    ),
    mapTo(inc()),
  );

What addToCounterEpic in the example above does is emitting the inc() action n times for every action of type add(n) that is received. For this the filterAction operator not only filters the action$ stream by the corresponding action type. It also infers the payload type of the action and makes it available in the the subsequent mergeMap((n: number) => ...) operator where n corresponds.

Contrary to redux-observable epickit doesn't provide you filtering actions by providing multiple actions types. But you can achieve the same with the following pattern:

const someEpic: Epic<S> = (action$) =>
  merge(
    action$.pipe(filterAction(add)),
    action$.pipe(filterAction(inc))
  );

Under the hood filterAction compares the reducers of two actions:

const a = { label: "", reducer: ((s) => s + 1), payload: undefined };
const b = { label: "", reducer: ((s, p) => s + p), payload: undefined };

const isEqual = a.reducer === b.reducer; // false

So reusing the same reducer breaks reliability of filterAction:

const reducer = (s) => 1;
const actionA = createAction(reducer);
const actionB = createAction(reducer); // using same redicer twice is not recommended

createEpicKit(...): {epic$, dispatch, state$, actions$}

@todo

dispatch(action: Action<State, Payload>): void

@todo

Full Example

The following fictive example contains two actions inc that increments a counter and add which adds a number to the counter. Furthermore it defines an epic that reacts to these both actions in porder to perform a debounced post request to an endpoint /counter. After a request has been finished the epic dispatches the action log for logging.


// definition of the application state
interface IState {
  counter: number;
  logs: string[];
}
const initialState: IState = {
  counter: 0,
  logs:[],
}

// actions with state reducer
const inc = createAction<IState>((s) => ({
  ...s,
  counter: s.counter + 1,
}));
// actions with reducer and payload
const add = createAction<IState, number>((s, n) => ({
  ...s,
  counter: s. counter + n,
}));
const log = createAction<IState, string>((s, m) => ({
  ...s,
  logs: [m, ...s.logs],
}));

// epic that makes an debounced request to endpoint "/counter"
// whenever "inc" or "add" action is being dispatched
// emits log action after each request 
const postCounterEpic: Epic<IState> = (action$, state$) =>
  merge(
    action$.pipe(filterAction(inc)),
    action$.pipe(filterAction(add)),
  )
  .pipe(
    debounceTime(2000),
    withLatestFrom(state$),
    mergeMap(([, state]) =>
      axios.post("/counter", state.counter),
    ),
    mapTo(log("counter uploaded")),
  );

const {dispatch, epic$} = createEpicKit(initialState, postCounterEpic);

// these actions will be queued
dispatch(inc());
dispatch(add(3));

// this starts the epic (queued actions will be flushed immediately)
const subscription = epic$.subscribe();

dispatch(add(-4));
dispatch(inc());

// this stops the epic after 5s
setTimeout(
  () => subscription.unsubscribe(),
  5000,
);