@leocode/rxstores
TypeScript icon, indicating that this package has built-in type declarations

0.1.1 • Public • Published

RxStores

Modular state manager for TS/JS apps, based on RxJS and focused on performance.

Prerequisites

  • Basic knowledge of RxJS

Installation

npm

npm i @leocode/rxstores

Yarn

yarn add @leocode/rxstores

Type definitions are built-in.

Main concepts

Store

Store acts as an observable data source for any view or view-model part of an app. A single store serves single data model. You can define your own interface here. It may be either only a facade for some complex business logic, or a container for some part of the logic as well.

By design, stores will expose two main properties:

  • data of RxJS Observable type
  • methods with logic-only interface exposed

One can use the logic interface either in the store instance itself, or using the methods property just for convention sake. See the example below.

Context

Context is a named container for stores. By default, the global one is used. One can select a custom one - see the example below.

Provider

Provider is a single, app-wide mechanism that exposes getters for stores and custom contexts.

Usage

Simple example

The simplest store one can create looks like this:

// stores/some.store.ts

import { Store } from "@leocode/rxstores";

export class SomeStore extends Store {
  init() {} // must be defined!
}

At this point, we have created the definition of SomeStore. Notice the init function, which is called right after the store is created. You might want to put here some initialization logic, e.g. API data fetching. Every store should implement initialization logic, even if empty.

Then, it can be used this way:

// index.js/ts

// define a callback function that will be called every time store's state changes
function onDataChange(data) {
  console.log("New data!", data);
}

// get a store from the global context
const store = Provider.getStore(SomeStore);
// or from a custom context
const store = Provider.from("some custom context").getStore(SomeStore);

// subscribe to the store with a callback function
const subscription = store.data$.subscribe(onDataChange);

// get store's current value using `value` property getter
const currentValue = store.value;

// emit a new value using `value` property setter
store.value = "some different value";

Provider lets us get a store directly from the global context, or specify a custom one with .from('context name').

This implementation doesn't do much, though. There's no initial data and no methods that could change the state. Let's fix this.

Defining a data model and initial value

TypeScript

// stores/some.store.ts

// define a model for data stored within the store below
export interface SomeModel {
  superImportantValue: number;
}

// create a store by extending base Store class and provide model type
export class SomeStore extends Store<SomeModel> {
  constructor() {
    const initialValue: SomeModel = { superImportantValue: 5 };

    // let base Store know that you want to provide an initial value
    super(initialValue);
  }

  init() {}
}

Now, we have provided an initial value for SomeStore. If initial value fetching logic is asynchronous, one can put it inside init method.

...

export class SomeStore extends Store<SomeModel> {
  ...

  // we can use async keyword here
  async init() {
    const remoteData = await fetch('/super-important-data')
      .then(r => r.json());
    this.value = remoteData;
  }
}

The issue is that view controllers are not able to know whether the data is loading, errored while loading, or is just non-existent. We can use a model helper class called Loadable.

import { Loadable } from '@leocode/rxstores/helpers';

...

export class SomeStore extends Store<SomeModel> {
  constructor() {
    super(Loadable.ofLoading<SomeModel>());
  }

  // we can use async keyword here
  async init() {
    try {
      const remoteData = await fetch('/super-important-data')
        .then(r => r.json());
      this.value = Loadable.ofData(remoteData);
    } catch (e) {
      this.value = Loadable.ofError(e);
    }
  }
}

Defining the logic interface

// stores/some.store.ts

export class SomeStore extends Store<SomeModel> {
  constructor() {
    super({ superImportantValue: 5 });
  }

  init() {}

  generateNewValue() {
    const newValue = Math.round(Math.random() * 10);

    this.value = newValue;
  }

  // all the methods go here
}

Accessing the interface

// index.js/ts

const someStore = Provider.getStore(SomeStore);

someStore.generateNewValue();
// or
someStore.methods.generateNewValue();

API

Store

Name Description isRequired
constructor Here you can pass an initial value for your class, like user object no
init Here goes all magic with creating your store. Put here code which has to be run at first, like downloading data, etc. yes
(get) value This getter returns the value stored in this specific store, you don’t have to declare it. It is inside, just use it with this.value yes
(get) methods This returns all method you declare in your class, this can return an empty object if you don’t have any methods no
(get) data$ This is your Observable, you should subscribe to it if you want to have a value up to date yes
(set) value Set data with this function in your class implementation no

Roadmap

While heads can be full of ideas, this library aims to be as modular and performant as possible. Thus, new features will be added to the project when it is obvious that no core rule is broken.

Current ideas include:

  • [ ] React bindings
  • [ ] Optional persistence

Package Sidebar

Install

npm i @leocode/rxstores

Weekly Downloads

12

Version

0.1.1

License

MIT

Unpacked Size

16.4 kB

Total Files

19

Last publish

Collaborators

  • soanvig
  • leocode-admin
  • tdzienniak