1 of 64

State Management for NativeScript Apps

by Alexander Vakrilov

2 of 64

Alex

3 of 64

4 of 64

5 of 64

6 of 64

7 of 64

8 of 64

Alex

ufsa

vakrilov

9 of 64

What is state

10 of 64

Login

Locations

Selected Location

Current Page

Markers

Map Viewport

11 of 64

12 of 64

13 of 64

Component Based Architecture

14 of 64

Componentized App

15 of 64

Manage State not UI

16 of 64

Managing DOM

$(() => {

$("#add-todo-item").on('click', (e) => {

var todoItem = $("#new-todo-item").val();

$("#todo-list").append(

"<li>" + todoItem +

"<button class='remove-btn'>Delete</button>" +

"</li>"

);

$("#new-todo-item").val("");

});

}

17 of 64

Managing State

// Template

<input [(ngModel)]="newTodo">

<button (click)="addTodo()">add</button>

<ul>

<li *ngFor="let todo of todos"> {{ todo }}</li>

</ul>

// JS

addTodo(){

this.todos.push(this.newTodo);

this.newTodo = "";

}

18 of 64

Data Projection

State

UI

19 of 64

Handle Changes

State

UI

20 of 64

Manage State not UI

21 of 64

“Just do it”

22 of 64

Map Component Template

<ActivityIndicator *ngIf="loading" …/>

<ns-map-list-item

*ngFor="let sensor of sensors"

[sensor]="sensor"

[selected]="sensor === currentSensor"

(select)="selectSensor(sensor)">

</ns-map-list-item>

23 of 64

Map Component Code

sensors: Array<Sensor>;

currentSensor: Sensor;

loading: boolean;

constructor(public service: SensorService) { }

selectSensor(selected: Sensor) {

this.currentSensor = selected;

}

ngOnInit(): void {

this.loading = true;

this.service.getItems()

.subscribe((items) => {

this.sensors = items;

this.loading = false;

});

}

State

Mutate State

24 of 64

25 of 64

26 of 64

Map Component UI

<GridLayout>

<Mapbox (mapReady)="onMapReady($event)" …/>

<!-- rest of the ui -->

</GridLayout>

27 of 64

Adding Map Markers

onMapReady(args): void {

this.mapView = args.map;

const marker = <MapboxMarker>{

id: 1,

lat: 52.3602160,

lng: 4.8891680,

title: 'One-line title here'};

this.mapView.addMarkers([marker]);

}

// select marker

marker.update({ ...marker, selected: true });

// delete marker

this.mapView.removeMarkers([marker.id]);

28 of 64

Just Code It

29 of 64

… 2 hours later

(summarized in 9 slides)

30 of 64

Result

31 of 64

32 of 64

Manage State not UI

and

33 of 64

@Component({

selector: "ns-map-marker"

})

export class MapMarkerComponent {

@Input() title: string;

@Input() lat: number;

@Input() lng: number;

//…

private marker: MapboxMarker;

ngOnChanges(changes: SimpleChanges): void {

// handle updates

}

ngOnDestroy(changes: SimpleChanges): void {

// remove updates

}

}

34 of 64

@Component({

selector: "ns-map-marker"

})

export class MapMarkerComponent {

@Input() title: string;

@Input() lat: number;

@Input() lng: number;

//…

private marker: MapboxMarker;

ngOnChanges(changes: SimpleChanges): void {

// handle updates

}

ngOnDestroy(changes: SimpleChanges): void {

// remove updates

}

}

<Mapbox (mapReady)="mapBoxViewApi = $event.map">

<map-marker

*ngFor="let sensor of sensors$ | async"

[mapBoxViewApi]="mapBoxViewApi"

[title]="sensor.name"

[lat]="sensor.location.lat"

[lng]="sensor.location.lng"

(tap)="selectSensor(sensor)" … >

</map-marker>

</Mapbox>

35 of 64

36 of 64

We don’t live in a perfect world

37 of 64

Adopt a State Management Approach

38 of 64

39 of 64

Why

  • Unified way to work
  • Single source of truth
  • One-way data flow

40 of 64

41 of 64

Why

  • Unified way to work
  • Single source of truth
  • One-way data flow
  • Derived data + query
  • Immutability/performance
  • Devtools

42 of 64

Why Not

  • Learning curve
  • Boilerplate code / More files

43 of 64

Make HMR Work for You

44 of 64

45 of 64

let cachedUrl: string;

onBeforeLivesync.subscribe(moduleRef => {

const router = <Router>moduleRef.injector.get(Router);

cachedUrl = router.url;

});

onAfterLivesync.subscribe(({ moduleRef, error }) => {

const router = <RouterExtensions>moduleRef.injector.get(RouterExtensions);

router.navigateByUrl(cachedUrl, { animated: false, clearHistory: true });

});

46 of 64

47 of 64

Single Source Of Truth

48 of 64

Single Source Of Truth

to save and load

49 of 64

import { persistState } from '@datorama/akita';

let cache = {};

const inMemoryStorage = {

setItem: (key, value) => cache[key] = value,

getItem: (key) => cache[key],

clear: () => cache = {}

};

onBeforeLivesync.subscribe(moduleRef => {

// ...

persistState({ storage: inMemoryStorage });

});

50 of 64

51 of 64

Re-cap

52 of 64

Lessons Learned

  • Manage state not UI
  • Adopt a state management approach
  • Make HMR work for you

53 of 64

Thanks!

ufsa

54 of 64

Some Text

Some Text

Right

Some Text

Left

55 of 64

55

SlidesCarnival icons are editable shapes.

This means that you can:

  • Resize them without losing quality.
  • Change line color, width and style.

Isn’t that nice? :)

Examples:

56 of 64

57 of 64

createMarker(s: Sensor): MapboxMarker {

return {

id: s.id,

...s.location,

title: s.name,

onTap: () => this.selectSensor(s),

};

}

58 of 64

createMarker(s: Sensor): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

const markers = this.sensors.map(this.createMarker);

this.mapView.addMarkers(markers);

}

59 of 64

createMarker(s: Sensor): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

this.renderMap();

}

renderMap(args): void {

if (!this.mapView || !this.sensors) return;

const markers = this.sensors.map(this.createMarker);

this.mapView.addMarkers(markers);

}

60 of 64

createMarker(s: Sensor): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

this.renderMap();

}

ngOnInit(): void {

this.loading = true;

this.service.getItems().subscribe((items) => {

this.sensors = items;

this.loading = false;

this.renderMap();

});

}

renderMap(args): void {

if (!this.mapView || !this.sensors) return;

const markers = this.sensors.map(this.createMarker);

this.mapView.addMarkers(markers);

}

61 of 64

createMarker(s: SensorVM): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

this.renderMap();

}

ngOnInit(): void {

this.loading = true;

this.service.getItems().subscribe((items) => {

this.sensors = items;

this.loading = false;

this.renderMap();

});

}

type SensorVM = Sensor & { marker?: MapboxMarker };

renderMap(args): void {

if (!this.mapView || !this.sensors) return;

// Create markers for each sensor

this.sensors.forEach((sensor) => {

sensor.marker = this.createMarker(sensor);

});

this.mapView.addMarkers(

this.sensors.map(s => s.marker)

);

}

62 of 64

createMarker(s: SensorVM): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

this.renderMap();

}

ngOnInit(): void {

this.loading = true;

this.service.getItems().subscribe((items) => {

this.sensors = items;

this.loading = false;

this.renderMap();

});

}

type SensorVM = Sensor & { marker?: MapboxMarker };

renderMap(args): void { … }

63 of 64

createMarker(s: SensorVM): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

this.renderMap();

}

ngOnInit(): void {

this.loading = true;

this.service.getItems().subscribe((items) => {

this.sensors = items;

this.loading = false;

this.renderMap();

});

}

type SensorVM = Sensor & { marker?: MapboxMarker };

renderMap(args): void { … }

selectSensor(selected: SensorVM) {

const isSame = this.currentSensor === selected;

if (!isSame) {

selected.marker.update({

...selected.marker,

selected:true

});

}

this.currentSensor = selected;

}

64 of 64

createMarker(s: SensorVM): MapboxMarker { … }

onMapReady(args): void {

this.mapView = args.map;

this.renderMap();

}

ngOnInit(): void {

this.loading = true;

this.service.getItems().subscribe((items) => {

this.sensors = items;

this.loading = false;

this.renderMap();

});

}

type SensorVM = Sensor & { marker?: MapboxMarker };

renderMap(args): void { … }

selectSensor(selected: SensorVM) { … }

addSensor(selected: SensorVM) { … }

removeSensor(selected: SensorVM) { … }