State Management
Kumar Ratnam Pandey
Software Engineer at
GeekyAnts
@ratnam99
Why
State Management?
-
State Synchronization
-
Re-rendering
Why State Management?
Type of states:
- Server state.
- Persistent state.
- The URL and router state.
- Client state.
- Transient client state.
- 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!
Copy of State Managment In Angular
By Chetan Hegde
Copy of 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.
- 202