-
Notifications
You must be signed in to change notification settings - Fork 7
Store
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).
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));
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
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
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.
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()
.