State Management

Kumar Ratnam Pandey

Software Engineer at 

GeekyAnts

@ratnam99

Why

State Management?

  • State Synchronization

     
  • Re-rendering

Why State Management?

Type of states:
 

  1. Server state.
     
  2. Persistent state.
     
  3. The URL and router state.
     
  4. Client state.
     
  5. Transient client state.
     
  6. Local UI state.

State Synchronization

  • The persistent state and the server state store the same information. So do the client state and the URL. Because of this we have to synchronize them.

State Synchronization

Re-rendering

Data Layer

UI

Changes

Changes

Example

  • The application has two main routes: one that displays a list of contacts, and the other one showing detailed information about each contact.
export interface Contact {
  id: number;
  name: string;
  number: string;
  email: string;
  isFav: boolean;
}

export interface Filters {
  name: string;
  fav: boolean;
}

Application Model

@Injectable()
export class ApiCalls {
  _contacts: {[id:number]: Contact} = {};
  _list: number[] = [];

  filters: Filters = {name: null, fav: false};

  constructor(private http: Http) {}

  get contacts(): Contact[] {
    return this._list.map(n => this._contacts[n]);
  }

  findContact(id: number): Observable<Talk> {
    return of(this._contacts[id]);
  }

  makeFav(id: number, fav: number): void {
    const contact = this._contacts[id];
    contact.isFav = fav;
    this.http.post(`/fav`, {id: contact.id, isFav: fav}).forEach(() => {});
  }

  changeFilters(filters: Filters): void {
    this.filters = filters;
    this.refetch();
  }

  private refetch(): void {
    const params = new URLSearchParams();
    params.set("name", this.filters.name);
    params.set("fav", this.filters.fav);
    this.http.get(`/contacts`, {search: params}).forEach((r) => {
      const data = r.json();
      this._contacts = data.contacts;
      this._list = data.list;
    });
  }
}

Backend

export class WatchService {
  watched: {[k:number]:boolean} = {};

  watch(contact: Contact): void {
    console.log("watch", contact.id);
    this.watched[contact.id] = true;
  }

  isWatched(contact: Contact): boolean {
    return this.watched[contact.id];
  }
}

Watch Service

Everything looks fine?

  • Syncing Persistent and Server State.

Problems

  • Syncing URL and Client State

Problems

State Management?

Let's see what

manages each type

of state in this application.

Refactoring 1:

 

 

Separating State Management

Architecture

3 simple steps

Defining all the actions our application can perform.

Eg-: export type Filter = { 
                    type: 'FILTER',
                    filters: Filters 
                };

Step 1

Then the state.

Step 2


// all non-local state of the application
          export type State = {
                       contacts: { [id: number]: Contact },
                       filters: Filters,
          };
// init state
          export const initState: State = {
                              contacts: {},
                              filters: {fav: null}
          };

And, finally, the reducer.

Step 3

// a factory to create reducer
export function reducer(backend: Backend, watch: WatchService) {
  return (store: Store<State, Action>, state: State, action: Action) => {
    switch (action.type) {
      case 'FILTER':
        return backend.findContacts(action.filters).
            map(r => ({...state, ...r, filters: action.filters}));
     case 'SHOW_DETAIL':
        if (state.contacts[action.contactId]) return state;
        return backend.findContact(action.contactId).
            map(c => ({...state, contacts: {...state.contacts, [c.id]: c}));
     default:
        return state;
    }
  }
}
  • State management and computation/services are separated. The reducer is the only place where we manipulate non-local state.

     
  • We no longer use mutable objects for persistent and client state.

Analysis

Redux should be the means of achieving a goal, not the goal

Alternatives

Mobx

2 simple steps

Making the store observable.

Eg: import {observable} from 'mobx';
    .
    .
    .
    export default filter observable=([
                       fav:boolean                             
                   ]);

Step 1

Make the component as an observer.

import {observer} from 'mobx';
.
.
.
@observer
export class ContactsAndFiltersComponent implements OnInit {
  contactList:Contact[]=[];
  current:string="Contact";
  filter:Filters={
    name:'',
    fav:false
  };
  constructor(public backend: Backend, private router: Router, private route: ActivatedRoute, private watchService:WatchService) {
  }

  viewContact(contact){
    this.watchService.id=contact.id;
    this.router.navigate(['contact']);
  }

  makeFav(contact:Contact){
    console.log("make Fav", contact);
    let newContactList=this.contactList.map((eachContact)=>{
      if(eachContact.id===contact.id){
        eachContact.isFav=!contact.isFav;
      }
      return eachContact;
    })
    this.contactList=newContactList;
    this.backend.makeFav(this.contactList);
  }

  viewFavlist(){
    let filters:Filters={
      name: this.filter.name,
      fav: true
    }
    this.router.navigate(['/contacts',this.createParams(filters)]);
  }

  viewAllContacts(){
    let filters:Filters={
      name: this.filter.name,
      fav: false
    }
    this.router.navigate(['/contacts',this.createParams(filters)]);
  }

  private createParams(filters: Filters): Params {
    const r:any = {};
    if (filters.name) r.name = filters.name;
    if (filters.fav) r.fav = filters.fav;
    return r;
  }

}

Step 2

Analysis

  • Less and clean code.

     
  • It's easier to make Async calls and handle side-effects.

     
  • With the flexibility of observers architecture can easily be spoilt.

     
  • A lot goes behind the scene

NgRx

4 simple steps

Defining the state of our application.

Eg-: export const initState: State = {
                 contacts: {},
                 filters: {fav: null}
              };

Step 1

Defining all the actions our application can perform.

Eg-: export type Filter = { 
                    type: 'FILTER',
                    filters: Filters 
                };

Step 2

Next, the effects class.

class TalksEffects {
  // @Effect() navigateToContacts = ...
  // @Effect() navigateToContact = ...
  // @Effect() favContact = ...
  constructor(
               private actions: Actions,
               private store: Store<State>,
               private backend: Backend,
               private watch: WatchService) {
  }
}

Step 3

Then, the reducer.

Step 4

function appReducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case 'CONTACTS': // ...
    case 'CONTACT_UPDATED': // ...
    case 'FAV': // ...
    case 'UNFAV': // ...
    default: return state;

  }
}
  • If the router needs some information from Backend, it cannot reliably get it.
     
  • if Backend needs something from the router or the URL, it cannot reliably get it.
     
  • The reducer cannot stop the navigation.
     
  • The synchronization is ad-hoc. If we add a new route, we will have to reimplement the synchronization code there as well.

Remaining Problems

Refactoring 2:

 

 

Store and Router.

Make navigation part of updating the store.
And finally we can make updating the store part of navigation.

imports: [
    //...
    RouterConnectedtoStore.forRoot(
      "reducer",
      [
        { path: '', pathMatch: 'full', redirectTo: 'talks' },
        { path: 'contacts',  component: ContactsAndFiltersCmp },
        { path: 'contact/:id', component: ContactDetailsCmp }
      ]
    )

  ],

RouterConnectedToStoreModule will set up the router in such a way that right after the URL gets parsed and the future router state gets created, the router will dispatch the an appropriate action (say STORE_ROUTE_NAVIGATION).

Refactoring 2: Store and Router.

Then we define the case for STORE_ROUTE_NAVIGATION action inside the reducer function.

Refactoring 2: Store and Router.

// a factory to create reducer
export function reducer(backend: Backend, watch: WatchService) {
  return (store: Store<State, Action>, state: State, action: Action) => {
    switch (action.type) {
      case 'STORE_ROUTE_NAVIGATION':
        // Logic for proper navigation
      case 'SHOW_DETAIL':
        if (state.contacts[action.contactId]) return state;
        return backend.findContact(action.contactId).
            map(c => ({...state, contacts: {...state.contacts, [c.id]: c}));
      default:
        return state;
    }
  }
}
  • This refactoring tied the client state to the URL. The router navigation invokes the reducer, and then once the reducer is done, the navigation proceeds using the new state.

Analysis

  • The main takeaway is you should be deliberate about how you manage state. It is a hard problem, and hence it requires careful thinking. Do not trust anyone saying they have “one simple pattern/library” fixing it — that’s never the case.
     

  • Decide on the types of state, how to manage them, and how to make sure the state is consistent. Be intentional about your design.

Takeaway

THANK YOU!

State Managment In Angular

By Ratnam Pandey

State Managment In Angular

This slide focuses on 3 questions- Why, What and How? Why State Management? What is State Management and How states can be managed. There is a small walkthrough of Redux, Mobx and NgRx.

  • 1,056