Application state
What is state?
Data
UI
- Server data
- Request params
- Router info
- Global state
- Open/Closed
- Active/Inactive
- Form values
- Window size
- Server data
- Request params
- Loading status
Shared
@Component({ selector: 'app-filter', template: ` <input type="checkbox" [checked]="isActive" (change)="toggle()" /> `, ... }) export class FilterComponent { isActive: boolean; toggle(): void { this.isActive = !this.isActive; } }
Filter
Local UI state
@Component({ selector: 'app-filter', template: ` <input type="checkbox" [checked]="isActive" (change)="toggle()" /> `, ... }) export class FilterComponent { @Input() isActive: boolean; @Output() activate = new EventEmitter<void>(); @Output() deactivate = new EventEmitter<void>(); toggle(): void { if(this.isActive) { this.deactivate.emit(); } else { this.activate.emit(); } } }
Filter
Decoupled from state
@Component({ selector: 'app-filters', template: ` <app-filter *ngFor="let filter of filters" [isActive]="isActive(filter)" (activate)="activate(filter)" (deactivate)="deactivate(filter)" `, ... }) export class FiltersComponent<T extends { id: string }> { filters: T[]; private activateFilters: string[] = []; activate(filter: T) { this.activateFilters.push(filter.id); } deactivate(filter: T) { const index = this.activateFilters.indexOf(filter.id); if(index !== -1) { this.activateFilters.splice(index, 1); } } isActive(filter: T) { return this.activateFilters.some(id => id === filter.id); } }
Filters
Local UI state
local state
Array lookup each event
Array lookup each render
Data normalization
const filters: T[] = [ filter1, filter2, filter3, ... filter10, ]; const activeFilterIds: string[] = [ 'filter8', 'filter9', 'filter10' ]; function isActive(filter: T) { return activeFilters.some(id => id === filter.id); } const activeFilters = filters.filter(isActive); // 1 * 1 + 1 * 2 + 1 * 3 + 7 * 3 = 28 checks
Data normalization
Problem
const filters: T[] = [ filter1, filter2, filter3, ... filter10, ]; const activeFilterIds: HashMap<boolean> = { 'filter8': true, 'filter9': true, 'filter10': true }; function isActive(filter: T) { return activeFilters[filter.id] === true; } const activeFilters = filters.filter(isActive); // 10 * 1 = 10 === checklist.length checks
Data normalization
HashMap
interface HashMap<T> { [key: string]: T; }
@Component({ selector: 'app-filters', template: ` <app-filter *ngFor="let filter of filters" [isActive]="isActive(filter)" (activate)="activate(filter)" (deactivate)="deactivate(filter)" `, ... }) export class FiltersComponent<T extends { id: string }> { filters: T[] = []; private activateFilterIds: HashMap<boolean> = {}; activate(filter: T) { this.activateFilterIds[filter.id] = true; } deactivate(filter: T) { this.activateFilterIds[filter.id] = false; } isActive(filter: T) { return this.activateFilterIds[filter.id] === true; } }
Filters
Local UI state
@Component({ selector: 'app-filters', template: ` <app-filter *ngFor="let filter of filters" [isActive]="isActive(filter)" (activate)="activate(filter)" (deactivate)="deactivate(filter)" `, ... }) export class FiltersComponent<T extends { id: string }> { filters: T[] = []; private activateFilters: string[] = []; activate(filter: T) { this.activateFilters.push(filter.id); } deactivate(filter: T) { const index = this.activateFilters.indexOf(filter.id); if(index !== -1) { this.activateFilters.splice(index, 1); } } isActive(filter: T) { return this.activateFilters.some(id => id === filter.id); } }
const normalizedData: NormalizedCollection<T> = { values: { 'filter1': filter1, 'filter2': filter2, // ... 'filter10': filter10 }, ids: [ 'filter1', 'filter2', // ... 'filter10' ]; }; const collection = toArray(normalizedData); const normalized = normalize(collection);
Data normalization
Sorted normalized collections
interface Normalized<T> { values: HashMap<T>; ids: string[]; } function normalize<T>(collection: T[]): Normalized<T> { return collection.reduce(({values, ids}, item) => ({ values: { ...values, [item.id]: item }, ids: ids.concat(item.id) }), { values: {}, ids: []}); } function toArray<T>({ values, ids }: Normalized<T>): T[] { return ids.map(id => values[id]); }
Array
Normalized
// Get by id const result = data.values[id]; // Get by ids (unordered) const byIds = ids.map(id => data.values[id]); // Get by ids (ordered) const byIdsOrdered = ids.map(id => data.values[id]);
// Get by id const i = data.indexOf(id); const byId = data[i]; // Get by ids (unordered) const byIds = data.filter(id => ids.indexOf(id) !== -1 ); // Get by ids (ordered) const byIdsOrdered = ids.map(id => { const i = data.indexOf(id); return data[i]; });
Service
Server
T[]
Normalize data
Normalized<T>
...
interface FiltersResponse<T> { filters: T[]; } interface FiltersData<T> { filters: Normalized<T>; } @Injectable() export class FiltersService<T> { constructor(private http: HttpClient) {} get(): Observable<FiltersData<T>> { return this.http.get('...') .pipe( map(({ filters }: FiltersResponse<T>) => ({ filters: normalize(filters) })) ); } }
FiltersService
Normalized state
Service
Component
Server
T[]
Normalize data
Selector
Normalized<T>
Data enrichment
interface Filter { id: string; // ... } interface FiltersData { filters: Normalized<Filter>; activeFilters: HashMap<boolean>; }
Filters selector
interface EnrichedFilter extends Filter { active: boolean; } interface FiltersState { filters: EnrichedFilter[]; } function selectState(data: FiltersData) { const { filters, activeFilters } = data; const { values, ids } = filters; return { filters: ids.map(id => ({ ...values[id], active: activeFilters[id] })) as EnrichedFilter[] } }
@Component({ selector: 'app-filters', template: ` <ng-container *ngIf="state$ | async as state"> <app-filter *ngFor="let filter of state.filters" [isActive]="filter.active" (activate)="activate(filter)" (deactivate)="deactivate(filter)" </ng-container> `, // ... }) export class FiltersComponent implements OnInit { state$: Observable<FiltersState>; constructor(private service: FiltersService) {} ngOnInit() { this.state$ = this.service.get() .pipe( map(selectFiltersState) ); } // ... }
Filters
@Component({ selector: 'app-filters', template: ` <ng-container *ngIf="data$ | async as data"> <app-filter *ngFor="let filter of data.filters" [isActive]="isActive(filter, data.activeFilters)" (activate)="activate(filter)" (deactivate)="deactivate(filter)" </ng-container> `, // ... }) export class FiltersComponent implements OnInit { data$: Observable<FiltersData>; constructor(private service: FiltersService) {} ngOnInit() { this.data$ = this.service.get(); } isActive(filter: Filter, activeFilters: HashMap<boolean>) { return activeFilters[filter.id] === true; } // ... }
// interface EnrichedFilter extends Filter { // active: boolean; // } interface FiltersState { // filters: EnrichedFilter[]; activeFilters: EnrichedFilter[]; } function selectState(data: FiltersData) { // const { filters, activeFilters } = data; // const { values, ids } = filters; // return { // filters: ids.map(id => ({ // ...values[id], // active: activeFilters[id] // })) as EnrichedFilter[], activeFilters: Object.keys(activeFilters) .map(id => values[id]) // } }
Active filters
@Component({ selector: 'app-filters', template: ` <ng-container *ngIf="state$ | async as state"> ... <div *ngFor="let filter of state.activeFilters"> Active filter: {{ filter.id }} </div> </ng-container> `, // ... }) export class FiltersComponent implements OnInit { // state$: Observable<FiltersState>; // constructor(private service: FiltersService) {} // ngOnInit() { // this.state$ = this.service.get() // .pipe( // map(selectFiltersState) // ); // } // ... }
Optimistic state
Service
Component
Server
T[]
Normalize data
Selector
Normalized<T>
Service
UI
- Single source of truth
- Server interaction
- Derived from data
- Instant
- Instant
- Server interaction
Optimistic
Service
Component
Validated state
Updates
Initial state
const updates = {}; const validatedState = { activeFilters: { 'filter1': true }, // ... } // Merge validated state + updates const state = { isOptimistic: false, activeFilters: { 'filter1': true }, // ... }
Validated state
Validated state
Service
Component
Validated state
Updates
Update
Update
const updates = { activeFilters: { 'filter2': true }, }; const validatedState = { activeFilters: { 'filter1': true }, // ... } // Merge validated state + updates const state = { isOptimistic: true, activeFilters: { 'filter1': true, 'filter2': true }, // ... }
Optimistic state
Optimistic state
Service
Component
Validated state
Updates
Validation
Update
const optimisticState = { isOptimistic: true, activeFilters: { 'filter1': true, 'filter2': true // update }, // ... } // Successful call validatedState = optimisticState; updates = {}; // Merge validated state + updates const state = { isOptimistic: false, activeFilters: { 'filter1': true, 'filter2': true }, // ... }
Validated state
Validated state
Service
Component
Validated state
Updates
Rejection
Update
const optimisticState = { isOptimistic: true, activeFilters: { 'filter1': true, 'filter2': true // update }, // ... } // Failed call updates = {}; // Merge validated state + updates const state = { isOptimistic: false, activeFilters: { 'filter1': true }, // ... }
Validated state
Validated state
Error
Title Text
Application state
deck
By rachnerd
deck
- 190