State Management

If you’ve ever maintained a less-than-stellar Angular project, you’ll find that the logic of your code is softened into components, and that if components aren’t properly split, they are copied into paste code. For example:

/ / Template simple display user {{user | json}}Copy the code
onChangeUser(id: string) { // Call API to change user } onRefreshUser() { // Call API to change user } ... . Various other actionsCopy the code

As a result, we have to navigate through various methods to see how the data is being modified, and to figure out the relationship between methods, which is very painful. This leads to the concept of one-way data flow, where changes to the data state are stripped away and the View layer, the Component layer, always does two things:

  • Read current state
  • Tell State Management how to modify state

This helps our Component to detaches itself from the complexity of the business logic and focus only on displaying the data, known as the View layer.

We usually call the state management layer a Store

What is RxJS about State Management? If you’re familiar with React, there’s a trick in React called ContainerComponent and PresentaionComponent, where ContainerComponent wraps the state, The data is then passed to the PresentaionComponent.

Why is that?

If we use state directly in Component, there is no way to notify Component that the data has changed. We can only use ContainerComponent to wrap the corresponding data into different props. This allows the Component’s props to notify the Child Component that the data has changed.

However, when it comes to notifying data changes, you can probably see that this is RxJS’s fortificant. If you implement the Store with RxJS, you can avoid the complexity of calling ContainerComponent and simplify the learning process for Redux. (Of course, React can now do something similar with Hooks, which are related to RxJS.)

How to implement State Management

First of all, let’s see, what is a Store?

In order to implement one-way data flow, we need the store to satisfy the following functions:

  • You can get the state from the Store and have a way to notify when the state changes.
  • You can modify state through Store, but you cannot modify state directly. You can only tell Store what changes you want to make. Store should handle the specific business logic.

If you have some background on RxJS, you can see that implementing State at BehaviorSubject is a good fit.

So let’s go throughBehaviorSubjectImplement a simplestateInterface:

class Store { state$: Observable<State>; private _state$: BehaviorSubject<State>; constructor(initialState: State) { this._state$ = new BehaviorSubject<State>(initialState); this.state$ = this._state$.asObservable(); } get state() { return this._state$.value; }}Copy the code
  • State Can obtain the latest state value
  • State $can emit changes when state changes
  • Furthermore, state cannot be directly modified externally

So the question is, how do we change state? We can define a dispacth method that always passes only the Action name to tell the store that it needs to trigger the corresponding change logic.

_action$ = new Subject<Action>(); / /... dispatch(action: Action) { this._action$.next(action); }Copy the code

Thus, when we turn the Action into a stream, the state change becomes surprisingly easy. The relationship becomes that the state changes when the action$emit value is generated. It is not difficult to write the following code

this._action$.pipe( map(action => { if (action === 'updateUser') { // return new state. } if (action === 'updateUser') {  // return new state. } }), ).subscribe(this._state$);Copy the code

It is not hard to see, in fact, this code in map is actually a reducer.

function reducer(state: State, action: Action) {
    if (action === 'updateUser') {
      // return new state.
    }
    if (action === 'updateUser') {
      // return new state.
    }
}
Copy the code

Modify the following code:

this._action$.pipe(scan(reducer, initialState))
Copy the code

Let’s look at the original class:

class Store { state$: Observable<State>; private _state$: BehaviorSubject<State>; private _action$ = new Subject<Action>(); constructor(initialState: State, reducer: Reducer) { this._state$ = new BehaviorSubject<State>(initialState); this.state$ = this._state$.asObservable(); this._action$.pipe(scan(reducer, initialState)).subscribe(this._state$); } get state() { return this._state$.value; } dispatch(action: Action) { this._action$.next(action); }}Copy the code

Now that we’ve implemented a simple Store, let’s look at how to implement a simple CRUD.

const reducer = function(state: State, action: Action) {
  if (action.name === 'Add') {
    return [
      ...state,
      new User(),
    ];
  }
  
  if (action.name === 'Delete') {
    const userId = action.payload.userId;
    const userIndex = state.findIndex(user => user.id === userId);
    return [
      ...state.slice(0, userIndex _ 1),
      ...state.slice(userIndex + 1),
    ];
  }
  
  if (action.name === 'Update') {
    const user = action.payload.user;
    const userIndex = state.findIndex(user => user.id === user.id);
    return [
      ...state.slice(0, userIndex _ 1),
      user,
      ...state.slice(userIndex + 1),
    ];
  }
}

const userStore = new Store([], reducer);
Copy the code

Of course, if you look at the implementation of NgRx, you will find that there are many optimizations in NgRx/Store. For example, the Action can be defined as an Object, and the if return or switch in Reducer can be implemented with ON. Of course, And then for asynchronous, Effects implementation.

In fact, we found that with RxJS we could implement a simple State Management with less code. Also, it is not necessary to Reducer — you can even omit the Reducer. In Angular, services make it easy to implement this read-write decoupling mode, preventing data from being changed in the same place without knowing where.

Pulling out a data layer with A service in Angular is personally recommended and lightweight to use. NgRx has also recently introduced a lightweight state solution called Component /Store.