Skip to content
Markus edited this page Apr 20, 2019 · 11 revisions

The Store is the main class that holds our state. The state is a single, plain object graph that holds all of an applications state. Just as Redux has only a single store, with reactive-state we only have a single root store. Although we create store slices to point to subgraphs of the state, each slice is linked to the root store and must not be seen as a separate store (so we do not have a multi-store approach as in Flux).

Creating the root store

const initialState = {
    counter: 0,
    todos: [
        { id: 0, description: "Walk the dog", done: false },
        { id: 1, description: "Do Homework", done: false }
    ]
};

const rootStore = Store.create(initialState);

rootStore.watch().subscribe(state => console.log("STATE CHANGED: ", state));

Adding Action/Reducer pairs

In order for a reducer to become active (read: produce a new state), we have to wire the reducer together with an action (that provides the payload data for the state update):

const incrementAction = new Subject<number>();
const incrementReducer = (state, payload) => ({ ...state, counter: state.counter + payload });
// string identifier is optional and only used for debugging/devtool
store.addReducer(incrementAction, incrementReducer, "INCREMENT");

store.select(state => state.counter).subscribe(counter => console.log("counter is: ", counter));
// (any subscription will fire immediately with the the last emitted state)
// console.log: counter is 0

incrementAction.next(1);
// console.log: counter is 1

incrementAction.next(1);
// console.log: counter is 2

Creating slices

Slices point to a specific subgraph of the root state and can be used to modularize your application and the reducers (see section Slicing the state for more detailed information):

const counterStore: Store<number> = rootStore.createSlice("counter"); // not a magic string, invalid keys will result in compiler errors!

const incrementAction = new Subject<number>();
const incrementReducer: Reducer<number, number> = (state, payload) => state + payload;
counterStore.addReducer(incrementAction, incrementReducer);

counterStore.watch().subscribe(counter => console.log("counter is ", counter);
// console.log: counter is 0

incrementAction.next(1);
// console.log: counter is 1

incrementAction.next(2);
// console.log: counter is 3

Destroying

When calling .destroy() on a store, all subscriptions to observables obtained via .select() or .watch() are completed (and thus unsubscribed) and the stores .destroyed property (of type Observable) is emitting and completes. This will also destroy all sub-slices created from that slice and thus unsubscribes all subscriptions on any created sub-slice!

const counterStore = rootStore.createSlice("counter");

const subscription = counterStore.select().subscribe(counter => console.log("counter is ", counter);
subscription.add(() => console.log("subscription is unsubscribed!");

counterStore.destroy();
// console.log: subscription is unsubscribed!

incrementAction.next(1); // has no effect anymore, reducer is not fired

A nice way of using destroy() function is to call it in the clean/destroy handler of a component to get rid of all subscriptions made to this slice (ngOnDestroy in Angular and componentWillUnmount or the return statement of the useEffect hook in React). That way you do not need to track every single subscription to that slice, as all subscriptions will automatically be unsubscribed.

Cloning

A clone is a copy of the store instance created via .clone() (still sharing the original state and registered reducers!). It is basically identical to a slice created via .createSlice(), but has the same state type as the store it was cloned from. This is mainly useful to terminate all subscriptions created via .select()/.watch(), (i.e. unsubscribe) automatically when the clone is destroyed via .destroy().