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

@hub-fx/core

v0.3.0-alpha.0

Published

State management with rxjs

Downloads

21

Readme

Hubfx Core (WIP)

Description

Reactive state management with RxJS.

Table of Contents

  1. Installation
  2. Core concepts
    1. Reactables
    2. Hub and Stores
    3. Effects
    4. Scoped Effects
    5. Flow & Containment
  3. Examples
    1. Basic Counter
    2. Scoped Effects - Updating Todos
    3. Connecting Multiple Hubs - Event Prices
  4. API
    1. Reactable
    2. RxBuilder
      1. RxConfig
    3. Other Interfaces
      1. Effect
      2. ScopedEffects
      3. Action
      4. Reducer
  5. Testing
    1. Reactables

Installation

npm i @hub-fx/core

Core concepts

Prerequisite: Basic understanding of Redux and RxJS is helpful.

In this documentation the term stream will refer to an RxJS observable stream.

Reactables

Reactables (prefixed with Rx) are objects that encapulate all the logic required for state management. They expose a state$ observable and actions methods. Applications can subscribe to state$ to receive state changes and call action methods to trigger them.

import { RxCounter } from '@hub-fx/examples';

const counterReactable = RxCounter();

const { state$, actions: { increment, reset } } = counterReactable;

state$.subscribe(({ count }) => {
  // Update the count when state changes.
  document.getElementById('count').innerHTML = count;
});

// Bind click handlers
document.getElementById('increment').addEventListener('click', increment);
document.getElementById('reset').addEventListener('click', reset);

For a full example, see Basic Counter Example.

Hub and Stores

Reactables are composed of Hubs & Stores.

The Hub is responsible for dispatching actions to the store(s) registered to the hub. It is also responsible for handling side effects. The main stream that initiates all actions and effects is the dispatcher$

Effects

When initializing a hub we can declare effects. The hub can listen for various actions and perform side effects as needed. Stores that are registered to the hub will be listening to these effects as well the dispatcher$.

Scoped Effects

Scoped Effects are dynamically created streams scoped to a particular action & key combination when an action is dispatch.

Flow & Containment

Actions and logic flow through the App in one direction and are contained in their respective streams. This makes state updates more predictable and traceable during debugging.

Avoid tapping your streams. This prevents logic leaking from the stream(s).

  • i.e do not tap stream A to trigger an action on stream B. Instead consider declaring stream A as a source for stream B so the dependency is explicit.

Examples

Basic Counter

Basic counter example. Button clicks dispatch actions to increment or reset the counter.

Basic Counter | Design Diagram | Try it out on StackBlitz. Choose your framework :-------------------------:|:-------------------------:|:-------------------------: | |

Scoped Effects - Updating Todos

Updating statuses of todo items shows scoped effects in action. An 'update todo' stream is created for each todo during update. Pending async calls in their respective stream are cancelled if a new request comes in with RxJS switchMap operator.

Todo Status Updates | Design Diagram | Try it out on StackBlitz. Choose your framework :-------------------------:|:-------------------------:|:-------------------------: | |

Connecting Multiple Hubs - Event Prices

This examples shows two sets of hubs & stores. The first set is responsible for updating state of the user controls. The second set fetches prices based on input from the first set.

Event Prices | Design Diagram | Try it out on StackBlitz. Choose your framework :-------------------------:|:-------------------------:|:-------------------------: | |

API

Reactable

Reactables provide the API for applications and UI components to receive and trigger state updates.

export interface Reactable<T, S = ActionMap> {
  state$: Observable<T>;
  actions?: S;
}

export interface ActionMap {
  [key: string | number]: (payload?: unknown) => void;
}

| Property | Description | | -------- | ----------- | | state$ | Observable that emit state changes | | actions (optional) | Dictionary of methods that dispatches actions to update state |

RxBuilder

RxBuilder factory help build Reactables. Accepts a RxConfig configuration object

type RxBuilder = <T, S extends Cases<T>>(config: RxConfig<T, S>) => Reactable<T, unknown>

RxConfig

Configuration object for creating Reactables.

interface RxConfig <T, S extends Cases<T>>{
  initialState: T;
  reducers: S;
  store?: boolean;
  debug?: boolean;
  effects?: Effect<unknown, unknown>[];
  sources?: Observable<Action<unknown>>[];
}

interface Cases<T> {
  [key: string]: SingleActionReducer<T, unknown>
    | {
        reducer: SingleActionReducer<T, unknown>
        effects?: (payload?: unknown) => ScopedEffects<unknown>
      };
}

type SingleActionReducer<T, S> = (state: T, action: Action<S>) => T;

| Property | Description | | -------- | ----------- | | initialState | Initial state of the Reactable | | reducers | Dictionary of cases for the Reactable to handle. Each case can be a reducer function or a configuration object. RxBuilder will use this to generate Actions, Reducers, and add ScopedEffects. | | debug (optional) | to turn on debugging to console.log all messages received by the store and state changes | | store (optional) | Option to store value if Reactable is used to persist application state. Subsequent subscriptions will received the latest stored value. Default to false | | effects (optional) | Array of Effects to be registered to the Reactable | | sources (optional) | Additional Action Observables the Reactable is listening to |

Debug Example:

Other Interfaces

Effect

Effects are expressed as RxJS Operator Functions. They pipe the dispatcher$ stream and run side effects on incoming Actions.

type Effect<T, S> = OperatorFunction<Action<T>, Action<S>>;

ScopedEffects

Scoped Effects are declared in Actions. They are dynamically created stream(s) scoped to an Action type & key combination.

interface ScopedEffects<T> {
  key?: string;
  effects: Effect<T, unknown>[];
}

| Property | Description | | -------- | ----------- | | key (optional) | key to be combined with the Action type to generate a unique signature for the effect stream(s). Example: An id for the entity the action is being performed on. | | effects | Array of Effects scoped to the Action type & key |

Example:


const UPDATE_TODO = 'UPDATE_TODO';
const UPDATE_TODO_SUCCESS = 'UPDATE_TODO_SUCCESS';
const updateTodo = ({ id, message }, todoService: TodoService) => ({
  type: UPDATE_TODO,
  payload: { id, message },
  scopedEffects: {
    key: id,
    effects: [
      (updateTodoActions$: Observable<Action<string>>) =>
        updateTodoActions$.pipe(
          mergeMap(({ payload: { id, message } }) => todoService.updateTodo(id, message))
          map(({ data }) => ({
            type: UPDATE_TODO_SUCCESS,
            payload: data
          }))
        )
    ]
  }
})

Action

interface Action<T = undefined> {
  type: string;
  payload?: T;
  scopedEffects?: ScopedEffects<T>;
}

| Property | Description | | -------- | ----------- | | type | type of Action being dispatched | | payload (optional) | payload associated with Action | | scopedEffects (optional) | See ScopedEffects |

Reducer

From Redux Docs

Reducers are functions that take the current state and an action as arguments, and return a new state result

type Reducer<T> = (state?: T, action?: Action<unknown>) => T;

Testing

We can use RxJS's built in Marble Testing for testing Reactables.

Reactables

// https://github.com/hub-fx/hub-fx/blob/main/packages/examples/src/Counter/Counter.test.ts
import { Counter } from './Counter';
import { Subscription } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';

describe('Counter', () => {
  let testScheduler: TestScheduler;
  let subscription: Subscription;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });
  afterEach(() => {
    subscription?.unsubscribe();
  });

  it('should increment and reset', () => {
    testScheduler.run(({ expectObservable, cold }) => {
      // Create Counter Reactable
      const {
        state$,
        actions: { increment, reset },
      } = Counter();

      // Call actions
      subscription = cold('--b-c', {
        b: increment,
        c: reset,
      }).subscribe((action) => action());

      // Assertions
      expectObservable(state$).toBe('a-b-c', {
        a: { count: 0 },
        b: { count: 1 },
        c: { count: 0 },
      });
    });
  });
});