DEV Community

Luca Zampetti
Luca Zampetti

Posted on

Easy immutable state management with RxComp RxJs and Immer

In this blog post we'll talk about RxComp Store a small Javascript module of RxComp, developed with Immer as a simple alternative to Redux.

The store can however be used with any framework or VanillaJS.


The store

With the useStore factory we create the immutable store with a default value.

The store will be consumed by a singleton service, and honoring the single-responsibility principle only a specific portion of the app data will be stored.

import { useStore } from 'rxcomp-store';

const { state$ } = useStore({ todolist: [] });

Subscribing to the state$ observable you always get the last immutable copy of the state draft.

state$.subscribe(state => this.todolist = state.todolist);

Reducing the store state

The reducer operator accept a reducer callback with the observable$ payload and a mutable draft of the state as parameter.

When reducer returns, an immutable copy of the state will be pushed to the state$ observable through Immer.

const { reducer } = useStore({ todolist: [] });

observable$.pipe(
  reducer((todolist, state) => state.todolist = todolist)
);

Catching errors into the state

The catchState operator is used to catch the error and store it in the immutable state.

const { catchState } = useStore({ todolist: [] });

observable$.pipe(
  catchState(console.log),
);

You can then observe the state for errors.

state$.subscribe(state => this.error = state.error);

Setting the busy state

The busy$ observable will store the busy flag in the immutable state and lock future calls until the observable completes.

const { busy$ } = useStore({ todolist: [] });

busy$().pipe(
  switchMap(() => observable$),
);

You can then observe the busy state.

state$.subscribe(state => this.busy = state.busy);

Loading state from Web Api Storage or Cookie

While reloading the page, you may want to reload the previous state of the app.

First we have to initialize the store with a different StoreType (the default is StoreType.Memory) and give it a unique store name.

import { StoreType, useStore } from 'rxcomp-store';

const { cached$ } = useStore({ todolist: [] }, 
  StoreType.Session, 'todolist'
);

With the cached$ observable we can retrieve the last saved state from sessionStorage or localStorage or cookie.

cached$((state) => state.todolist)

All together

  1. busy$ mark state as busy
  2. cached$ load data from cache
  3. reducer reduce the state to the new state
  4. catchState catch the error and reduce the state to the errored state.
import { StoreType, useStore } from 'rxcomp-store';

const { busy$, cached$, reducer, catchState } = useStore(
  { todolist: [] },
  StoreType.Session, 'todolist'
);

busy$().pipe(
  switchMap(() => 
    merge(cached$((state) => state.todolist), fromApi$).pipe(
      reducer((todolist, state) => state.todolist = todolist),
      catchState(console.log),
    )
  )
);

Querying the store state

The select$ observable accept a reducer callback with an immutable copy of the state as parameter and returns an immutable copy of a portion of the state as observable.

const { select$ } = useStore({ todolist: [] });

const todolist$ = select$((state) => state.todolist);

Querying with select

The select method works like the select$ observable but doesn't return an observable.

const { select } = useStore({ todolist: [] });

const todolist = select((state) => state.todolist);

In Conclusion

RxComp Store works with any framework or VanillaJS,

but is designed as a RxComp module.

If you want to know more about the integration with RxComp you can read the introduction article RxComp a modern ES6 alternative to Angular component framework.

If you got any idea, implementation or methods naming preferences feel free to contribute!

demo - codesandbox - repository


Other methods

Setting the store state

The next method accept a reducer callback with a mutable draft of the state as parameter.

When reducer returns, an immutable copy of the state will be pushed to the state$ observable through Immer.

It works like the reduce operator but doesn't return an observable.

const { next } = useStore({ todolist: [] });

next((state) => state.todolist = todolist))

Setting the store error

The nextError method will store the error parameter in the immutable state.
It works like the catchState operator but is intended to use in conjunction of classic catchError operator.

const { nextError } = useStore({ todolist: [] });

catchError(error => nextError(error))

Top comments (0)