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

deck

By rachnerd

deck

  • 155