Kumar Ratnam Pandey
Software Engineer at
GeekyAnts
@ratnam99
Why
State Management?
State Synchronization
Re-rendering
Data Layer
UI
Changes
Changes
Example
Server
Backend
Url / Router
WatchService
ContactsAndFiltersCmp
ContactsDetailsCmp
FiltersCmp
ContactsCmp
ContactCmp
FavBtnCmp
WatchBtnCmp
Server State
Persistent, Client, Transient Client and URL/ Router State
Local UI State
export interface Contact {
  id: number;
  name: string;
  number: string;
  email: string;
  isFav: boolean;
}
export interface Filters {
  name: string;
  fav: boolean;
}Application Model
@Component({
  selector: 'contact-details-cmp',
  templateUrl: './contact-details.html',
  styleUrls: ['./contact-details.css']
})
export class ContactDetailsCmp {
  contact: Contact;
  constructor(private backend: Backend,
              public watchService: WatchService,
              private route: ActivatedRoute) {
    route.params.mergeMap(p => this.backend.findContact(+p['id']))
    .subscribe(t => this.contact = t);
  }
  handleFav(fav:boolean): void {
    this.backend.makeFav(this.contact.id, fav);
  }
  handleWatch(): void {
    this.watchService.watch(this.contact);
  }@Component({
  selector: 'app-cmp',
  templateUrl: './contacts-and-filters.html',
  styleUrls: ['./contacts-and-filters.css']
})
export class ContactsAndFiltersCmp {
  constructor(public backend: Backend) {}
  handleFiltersChange(filters: Filters): void {
    this.backend.changeFilters(filters);
  }
}contacts-filters.ts
contacts-details.ts
@Injectable()
export class Backend {
  _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?
Problems
Problems
State Management?
 
Types
Let's see what
manages each type
of state in this application.
Refactoring 1:
Separating State Management
Introducing
Architecture
3 simple steps
Eg-: export type Filter = { 
                    type: 'FILTER',
                    filters: Filters 
                };2. Then the state.
// 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}
          };3. And, finally, the reducer.
// 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;
    }
  }
}Analysis
Redux should be the means of achieving a goal, not the goal
Alternatives
Introducing
Mobx
2 simple steps
Eg: import {observable} from 'mobx';
    .
    .
    .
    export default filter observable=([
                       fav:boolean                             
                   ]);2. 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;
  }
}Analysis
Introducing
NgRx
1. Defining the state of our application.
Eg-: export const initState: State = {
                 contacts: {},
                 filters: {fav: null}
              };2. Defining all the actions our application can perform.
Eg-: export type Filter = { 
                    type: 'FILTER',
                    filters: Filters 
                };3. 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) {
  }
}4. Then, the reducer.
function appReducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case 'CONTACTS': // ...
    case 'CONTACT_UPDATED': // ...
    case 'FAV': // ...
    case 'UNFAV': // ...
    default: return state;
  }
}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).
// 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;
    }
  }
}Then we define the case for STORE_ROUTE_NAVIGATION action inside the reducer function.
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