1.0.0-beta.3 • Published 1 month ago

@reactables/core v1.0.0-beta.3

Weekly downloads
-
License
ISC
Repository
-
Last release
1 month ago

Reactables Core

Description

Reactive state management with RxJS.

Table of Contents

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

Installation

Installation will require RxJS if not already installed.

npm i rxjs @reactables/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 '@reactables/examples';

const [state$, actions] = RxCounter();

const { increment, reset } = actions;

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 Store

Internally, Reactables are composed of a hub and store.

The hub is responsible for dispatching actions to the store. It is also responsible for handling side effects.

Effects

When initializing a Reactable we can declare effects. The hub will listen for various actions and perform side effects as needed. The store will receive actions resulting from these effects.

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.

Examples

Basic Counter

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

Design DiagramReactableTry it out on StackBlitz. Choose your framework
See Code for RxCounter

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.

Design DiagramReactableTry it out on StackBlitz. Choose your framework
See Code for RxTodoUpdates

Connecting Multiple Reactables - Event Tickets

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

Design DiagramReactableTry it out on StackBlitz. Choose your framework
See Code for RxEventTickets

API

Reactable

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

It is a tuple with the first item being an Observable emitting state changes and the second item is a dictionary of action methods for triggering state updates.

export type Reactable<T, S = ActionMap> = [Observable<T>, S];

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

RxBuilder

Factory function for building 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;
  storeValue?: boolean;
  debug?: boolean;
  effects?: Effect<unknown, unknown>[];
  sources?: Observable<Action<unknown>>[] | { [key: string]: Observable<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;
PropertyDescription
initialStateInitial state of the Reactable
reducersDictionary 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
storeValue (optional)Option to store value if Reactable is used to persist application state. Subsequent subscriptions will receive 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. Can be an array or a dictionary where key is the action type and value is the Observable emitting the payload

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 when defining reducers in RxConfig. They are dynamically created stream(s) scoped to an Action type & key combination.

interface ScopedEffects<T> {
  key?: string;
  effects: Effect<T, unknown>[];
}
PropertyDescription
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.
effectsArray of Effects scoped to the Action type & key

Action

interface Action<T = undefined> {
  type: string;
  payload?: T;
  scopedEffects?: ScopedEffects<T>;
}
PropertyDescription
typetype 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 Reactables

Flow Testing

You can test a series of actions to simulate a defined user flow with the testFlow method from @reactables/testing package. See @reactables/testing for details.

Marble Testing

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

Test for RxCounter

import { RxCounter } from './RxCounter';
import { Subscription } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';

describe('RxCounter', () => {
  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$,
        { increment, reset },
      ] = RxCounter();

      // 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 },
      });
    });
  });
});
1.0.0-beta.2

1 month ago

1.0.0-beta.3

1 month ago

1.0.0-beta.1

2 months ago

1.0.0-beta.0

2 months ago

0.8.0-alpha.2

2 months ago

0.8.0-alpha.3

2 months ago

0.8.0-alpha.0

2 months ago

0.7.1-alpha.4

2 months ago

0.7.1-alpha.2

2 months ago

0.7.1-alpha.1

2 months ago

0.7.1-alpha.3

2 months ago

0.7.0-alpha.19

2 months ago

0.7.0-alpha.18

2 months ago

0.7.0-alpha.16

2 months ago

0.7.0-alpha.17

2 months ago

0.7.0-alpha.15

2 months ago

0.7.0-alpha.14

3 months ago

0.7.0-alpha.12

3 months ago

0.7.0-alpha.11

3 months ago

0.7.0-alpha.10

3 months ago

0.7.0-alpha.9

3 months ago

0.7.0-alpha.7

3 months ago

0.7.0-alpha.6

3 months ago

0.7.0-alpha.8

3 months ago

0.7.0-alpha.1

4 months ago

0.7.0-alpha.3

4 months ago

0.7.0-alpha.2

4 months ago

0.7.0-alpha.5

4 months ago

0.7.0-alpha.4

4 months ago

0.7.0-alpha.0

4 months ago

0.6.0-alpha.0

5 months ago

0.5.4-alpha.0

5 months ago

0.5.3-alpha.0

5 months ago

0.5.0-alpha.0

5 months ago

0.5.1-alpha.0

5 months ago

0.5.2-alpha.0

5 months ago

0.4.6-alpha.0

5 months ago

0.4.5-alpha.0

5 months ago

0.4.4-alpha.0

5 months ago

0.4.2-alpha.0

5 months ago

0.4.3-alpha.0

5 months ago

0.4.3-alpha.1

5 months ago

0.4.1-alpha.1

5 months ago

0.4.1-alpha.0

5 months ago

0.4.0-alpha.3

5 months ago

0.4.0-alpha.2

5 months ago

0.4.0-alpha.1

5 months ago

0.4.0-alpha.0

5 months ago