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

rx-effects

v1.1.2

Published

Reactive state and effects management

Downloads

937

Readme

RxEffects

Reactive state and effect management with RxJS.

npm downloads types licence Coverage Status

Documentation

Installation

npm install rx-effects --save

Concepts

The main idea is to use the classic MVC pattern with event-based models (state stores) and reactive controllers (actions and effects). The view subscribes to model changes (state queries) of the controller and requests the controller to do some actions.

Main elements:

  • State – a data model.
  • Query – a getter and subscriber for data of the state.
  • StateMutation – a pure function which changes the state.
  • Store – a state storage, it provides methods to update and subscribe the state.
  • Action – an event emitter.
  • Effect – a business logic which handles the action and makes state changes and side effects.
  • Controller – a controller type for effects and business logic
  • Scope – a controller-like boundary for effects and business logic

State and Store

Define State

A state can be a primitive value or an object, and it is described as a type.

type CartState = { orders: Array<string> };

State Mutations

After that, it is recommended to declare a set of StateMutation functions which can be used to update the state. These functions should be pure and return a new state or the previous one. For providing an argument use currying functions.

Actually, StateMutation function can change the state in place, but it is responsible for a developer to track state changes by providing custom stateCompare function to a store.

const addPizzaToCart =
  (name: string): StateMutation<CartState> =>
  (state) => ({ ...state, orders: [...state.orders, name] });

const removePizzaFromCart =
  (name: string): StateMutation<CartState> =>
  (state) => ({
    ...state,
    orders: state.orders.filter((order) => order !== name),
  });

Creation of Store

A store is created by createStore() function, which takes an initial state:

const INITIAL_STATE: CartState = { orders: [] };
const cartStore: Store<CartState> = createStore(INITIAL_STATE);

Updating Store

The store can be updated by set() and update() methods:

  • set() applies the provided State value to the store.
  • update() creates the new state by the provided StateMutation and applies it to the store.
function resetCart() {
  cartStore.set(INITIAL_STATE);
}

function addPizza(name: string) {
  cartStore.update(addPizzaToCart(name));
}

Store.update() can apply an array of mutations while skipping empty mutation:

function addPizza(name: string) {
  cartStore.update([
    addPizzaToCart(name),
    name === 'Pepperoni' && addPizzaToCart('Bonus Pizza'),
  ]);
}

There is pipeStateMutations() helper, which can merge state updates into the single mutation:

import { pipeStateMutations } from './stateMutation';

const addPizzaToCartWithBonus = (name: string): StateMutation<CartState> =>
  pipeStateMutations([addPizzaToCart(name), addPizzaToCart('Bonus Pizza')]);

function addPizza(name: string) {
  cartStore.update(addPizzaToCartWithBonus(name));
}

Getting State

The store implements Query type for providing the state:

  • get() returns the current state.
  • value$ is an observable for the current state and future changes.

It is allowed to get the current state at any time. However, you should be aware how it is used during async functions, because the state can be changed after awaiting a promise:

// Not recommended
async function submitForm() {
  await validate(formStore.get());
  await postData(formStore.get()); // `formStore` can return another data here
}

// Recommended
async function submitForm() {
  const data = formStore.get();
  await validate(data);
  await postData(data);
}

State Queries

The store has select() and query() methods:

  • select() returns Observable for the part of the state.
  • value$ returns Query for the part of the state.

Both of the methods takes selector() and valueComparator() arguments:

  • selector() takes a state and produce a value based on the state.
  • valueComparator() is optional and allows change an equality check for the produced value.
const orders$: Observable<Array<string>> = cartStore.select(
  (state) => state.orders,
);

const ordersQuery: Query<Array<string>> = cartStore.query(
  (state) => state.orders,
);

Utility functions:

  • mapQuery() takes a query and a value mapper and returns a new query which projects the mapped value.
    const store = createStore<{ values: Array<string> }>();
    const valuesQuery = store.query((state) => state.values);
    const top5values = mapQuery(valuesQuery, (values) => values.slice(0, 5));
  • mergeQueries() takes queries and a value merger and returns a new query which projects the derived value of queries.
    const store1 = createStore<number>(2);
    const store2 = createStore<number>(3);
    const sumValueQuery = mergeQueries(
      [store1, store2],
      (value1, value2) => value1 + value2,
    );

Destroying Store

The store implements Controller type and has destroy() method.

destory() completes internal Observable sources and all derived observables, which are created by select() and query() methods.

After calling destroy() the store stops sending updates for state changes.

Usage of libraries for immutable state

Types and factories for states and store are compatible with external libraries for immutable values like Immer.js or Immutable.js. All state changes are encapsulated by StateMutation functions so using the API remains the same. The one thing which should be considered is providing the right state comparators to the Store.

Immer.js

Integration of Immer.js is straightforward: it is enough to use produce() function inside StateMutation functions:

Example:

import { produce } from 'immer';
import { StateMutation } from 'rx-effects';

export type CartState = { orders: Array<string> };

export const CART_STATE = declareState<CartState>({ orders: [] });

export const addPizzaToCart = (name: string): StateMutation<CartState> =>
  produce((state) => {
    state.orders.push(name);
  });

export const removePizzaFromCart = (name: string): StateMutation<CartState> =>
  produce((state) => {
    state.orders = state.orders.filter((order) => order !== name);
  });

Immutable.js

Integration of Immutable.js:

  • It is recommended to use Record and RecordOf for object-list states.
  • Use Immutable.is() function as a comparator for Immutable's state and values.

Example:

import { is, List, Record, RecordOf } from 'immutable';
import { declareState, StateMutation } from 'rx-effects';

export type CartState = RecordOf<{ orders: Immutable.List<string> }>;

export const CART_STATE = declareState<CartState>(
  Record({ orders: List() }),
  is, // State comparator of Immutable.js
);

export const addPizzaToCart =
  (name: string): StateMutation<CartState> =>
  (state) =>
    state.set('orders', state.orders.push(name));

export const removePizzaFromCart =
  (name: string): StateMutation<CartState> =>
  (state) =>
    state.set(
      'orders',
      state.orders.filter((order) => order !== name),
    );

Actions and Effects

Action

// TODO

Effect

// TODO

Controller

// TODO

Scope

// TODO


© 2021 Mikhail Nasyrov, MIT license