Imperative Interaction
1033 |{{state$ | async | json}}1034 | 1037 | `, 1038 | 1039 | providers: [StateService] 1040 | }) 1041 | export class AnyComponent implements OnDestroy { 1042 | state$ = this.stateService.state$; 1043 | 1044 | constructor(private stateService: StateService) { 1045 | 1046 | } 1047 | 1048 | updateState() { 1049 | this.stateService 1050 | .dispatch(({key: value})); 1051 | } 1052 | 1053 | } 1054 | ``` 1055 | 1056 | Why is this imperative? Imperative programming means working with instances and mutating state. 1057 | Whenever you write a `setter` or `getter` your code is imperative, It's not compose-able. 1058 | 1059 | If we now think about the `dispatch` method of `@ngrx/store`, we realize that it is similar to working with `setter`. 1060 | 1061 | While in this example it sits inside another un-compose-able thing, the instance method and therefore is ok. 1062 | Everything else would resolve in more refactoring. 1063 | 1064 | However, we can not use it to work with compose-able sources. 1065 | 1066 | Let's think about connection RouterState or any other source like `ngrx/store` to the local state: 1067 | 1068 | **Imperative Interaction Component** 1069 | ```typescript 1070 | @Component({ 1071 | selector: 'component', 1072 | template: ` 1073 |
Imperative Interaction
1074 |{{state$ | async | json}}1075 | `, 1076 | 1077 | providers: [StateService] 1078 | }) 1079 | export class AnyComponent implements OnDestroy { 1080 | subscription = new Subscription(); 1081 | state$ = this.stateService.state$; 1082 | 1083 | constructor(private stateService: StateService, 1084 | private store: Store) { 1085 | 1086 | this.subscription.add( 1087 | this.store.select(getStateSlice) 1088 | .subscribe(value => this.stateService 1089 | .setState({key: value}) 1090 | ) 1091 | ); 1092 | 1093 | } 1094 | 1095 | ngOnDestroy() { 1096 | this.subscription.unsubscribe(); 1097 | } 1098 | 1099 | } 1100 | ``` 1101 | 1102 | As we can see as soon as we deal with something compose-able setters don't work anymore. 1103 | We end up in a very ugly code. We break the reactive flow and we have to take care of subscriptions. 1104 | 1105 |  1106 | 1107 | But how can we go more declarative or even reactive? 1108 | **By providing something compose-able** :) 1109 | 1110 | Like an observable itself. :) 1111 | 1112 | By adding a single line of code we can go **fully declarative** as well as **fully subscription-less**. 1113 | 1114 |  1115 | 1116 | **Declarative Interaction Service** 1117 | ```typescript 1118 | export class StateService implements OnDestroy { 1119 | ... 1120 | private stateSubject = new Subject
Imperative Interaction
1143 |{{state$ | async | json}}1144 | `, 1145 | 1146 | providers: [StateService] 1147 | }) 1148 | export class AnyComponent { 1149 | state$ = this.stateService.state$; 1150 | 1151 | constructor(private stateService: StateService, 1152 | private store: Store) { 1153 | this.stateService.connectState( 1154 | this.store.select(getStateSlice) 1155 | .pipe(map(value => ({key: value}))) 1156 | ); 1157 | 1158 | } 1159 | } 1160 | ``` 1161 | 1162 | Let's take a detailed look at the introduced changes: 1163 | 1164 | 1. In `StateService` we changed 1165 | `stateSubject = new Subject<{ [key: string]: any }>();` 1166 | to 1167 | `stateSubject = new Subject
Declarative SideEffects
1229 | `, 1230 | providers: [StateAndEffectService] 1231 | }) 1232 | export class AnyComponent { 1233 | constructor(private stateService: StateAndEffectService) { 1234 | this.stateService.connectEffect(interval(1000) 1235 | .pipe(tap(_ => ({key: value})))); 1236 | } 1237 | 1238 | } 1239 | ``` 1240 | _(used RxJS parts: [publish](https://rxjs.dev/api/operator/publish) ))_ 1241 | 1242 | Note that the side-effect is now placed in a `tap` operator and the whole observable is handed over. 1243 | 1244 | ## Recap Problems 1245 | 1246 | So far we encountered the following problems: 1247 | - sharing work and references 1248 | - subscription handling 1249 | - late subscriber 1250 | - cold composition 1251 | - moving primitive tasks as subscription handling and state composition into another layer 1252 | - Subscription-less components and declarative interaction 1253 | 1254 | If you may already realize all the above problems naturally collapse into a single piece of code. 1255 | :) 1256 | 1257 | Also if you remember from the beginning this is what "the gang of four" quote says about Object-Oriented Design Patterns. 1258 | :) 1259 | 1260 | We can be happy as we did a great job so far. 1261 | We focused on understanding the problems, we used the language specific possibilities the right way, 1262 | and naturally, we ended up with a solution that is compact, robust and solves all related problems in an elegant way. 1263 | 1264 | Let's see how the local state service looks like. 1265 |  1266 | 1267 | 1268 | # Basic Usage 1269 | 1270 | ## Service Design 1271 | 1272 | **State Logic** 1273 | ```typescript 1274 | import {ConnectableObservable, merge, noop, Observable, OperatorFunction, Subject, Subscription, UnaryFunction} from 'rxjs'; 1275 | import {map, mergeAll, pluck, publishReplay, scan, tap} from 'rxjs/operators'; 1276 | 1277 | export function statefulComponent
1454 | ... 1455 | ` 1456 | }) 1457 | export class AnyComponent extends LocalState { 1458 | 1459 | constructor() { 1460 | this.super(); 1461 | } 1462 | 1463 | } 1464 | ``` 1465 | 1466 | **Injecting the service** 1467 | ```typescript 1468 | @Component({ 1469 | selector: 'component', 1470 | template: ` 1471 |Component
1472 | ... 1473 | `, 1474 | providers: [LocalState] 1475 | }) 1476 | export class AnyComponent { 1477 | 1478 | constructor(private stateService: LocalState) { 1479 | } 1480 | 1481 | } 1482 | ``` 1483 | 1484 | ## Service Usage 1485 | 1486 | Now let's see some basic usage: 1487 | 1488 | **Connecting Input-Bindings** 1489 | ```typescript 1490 | @Component({ 1491 | selector: 'component', 1492 | template: ` 1493 |Component
1494 | ... 1495 | ` 1496 | }) 1497 | export class AnyComponent extends LocalState { 1498 | 1499 | @Input() 1500 | set value(value) { 1501 | this.setState({slice: value}) 1502 | } 1503 | 1504 | constructor() { 1505 | this.super(); 1506 | } 1507 | 1508 | } 1509 | ``` 1510 | 1511 | **Connecting GlobalState** 1512 | ```typescript 1513 | @Component({ 1514 | selector: 'component', 1515 | template: ` 1516 |Component
1517 | ... 1518 | ` 1519 | }) 1520 | export class AnyComponent extends LocalState { 1521 | 1522 | @Input() 1523 | set value(value) { 1524 | this.setState({slice: value}) 1525 | } 1526 | 1527 | constructor(private store: Store) { 1528 | this.connectState( 1529 | this.store.select(getStateSlice) 1530 | .pipe(transformation) 1531 | ); 1532 | } 1533 | 1534 | } 1535 | ``` 1536 | 1537 | **Selecting LocalState** 1538 | ```typescript 1539 | @Component({ 1540 | selector: 'component', 1541 | template: ` 1542 |Component
1543 | 1544 | {{state$ | async}} 1545 | ` 1546 | }) 1547 | export class AnyComponent extends LocalState { 1548 | input$ = new Subject(); 1549 | 1550 | state$ = this.select( 1551 | withLatestFrom(input$), 1552 | map(([state, _]) => state.slice) 1553 | ); 1554 | 1555 | } 1556 | ``` 1557 | _(used RxJS parts: [withLatestFrom](https://rxjs.dev/api/operator/withLatestFrom))_ 1558 | 1559 | **Handling LocalSideEffects** 1560 | 1561 | ```typescript 1562 | @Component({ 1563 | selector: 'component', 1564 | template: ` 1565 |Component
1566 | ... 1567 | ` 1568 | }) 1569 | export class AnyComponent extends LocalState { 1570 | 1571 | constructor(private store: Store) { 1572 | this.connectEffect(interval(10000) 1573 | .pipe(tap(this.store.dispatch(loadDataAction))) 1574 | ); 1575 | } 1576 | 1577 | } 1578 | ``` 1579 | 1580 | This example shows a material design list that is collapsable. 1581 | It refreshed data every n seconds of if we click the button. 1582 | Also, it displays the fetched items. 1583 | 1584 | 1585 | *Basic Example - Stateful Component**: 1586 | ```typescript 1587 | @Component({ 1588 | selector: 'basic-list', 1589 | template: ` 1590 |someService.composedState$: {{someBadService.composedState$ | async | json}}
16 | someService.composedState$: {{someGoodService.composedState$ | async | json}}
16 | Imperative Interaction
8 |{{state$ | async | json}}9 | 12 | `, 13 | 14 | providers: [DeclarativeInteractionBadService] 15 | }) 16 | export class DeclarativeInteractionBadComponent { 17 | state$ = this.stateService.state$; 18 | 19 | constructor(private stateService: DeclarativeInteractionBadService) { 20 | 21 | } 22 | 23 | updateCount() { 24 | this.stateService 25 | .dispatch(({count: ~~(Math.random() * 100)})); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/examples/problems/declarative-interaction/declarative-interaction-bad.service.ts: -------------------------------------------------------------------------------- 1 | import {OnDestroy} from '@angular/core'; 2 | import {ConnectableObservable, Subject, Subscription} from 'rxjs'; 3 | import {map, publishReplay, scan} from 'rxjs/operators'; 4 | 5 | const stateAccumulator = (acc, [key, value]: [string, number]): { [key: string]: number } => ({...acc, [key]: value}); 6 | 7 | export class DeclarativeInteractionBadService implements OnDestroy { 8 | private stateSubscription = new Subscription(); 9 | private stateAccumulator = stateAccumulator; 10 | 11 | private stateSubject = new Subject<{ [key: string]: number }>(); 12 | state$ = this.stateSubject 13 | .pipe( 14 | // process single state change 15 | map(obj => Object.entries(obj).pop()), 16 | scan(this.stateAccumulator, {}), 17 | publishReplay(1) 18 | ) as ConnectableObservable
Declarative Interaction
10 |{{state$ | async | json}}11 | 14 | `, 15 | providers: [DeclarativeInteractionGoodService] 16 | }) 17 | export class DeclarativeInteractionGoodComponent { 18 | state$ = this.stateService.state$; 19 | update$ = new Subject(); 20 | 21 | constructor(private stateService: DeclarativeInteractionGoodService) { 22 | this.stateService.connectSlice(this.update$ 23 | .pipe(map(_ => ({count: ~~(Math.random() * 100)})))); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/examples/problems/declarative-interaction/declarative-interaction-good.service.ts: -------------------------------------------------------------------------------- 1 | import {OnDestroy} from '@angular/core'; 2 | import {ConnectableObservable, Observable, Subject, Subscription} from 'rxjs'; 3 | import {map, mergeAll, publishReplay, scan} from 'rxjs/operators'; 4 | 5 | const stateAccumulator = (acc, [key, value]: [string, number]): { [key: string]: number } => ({...acc, [key]: value}); 6 | 7 | export class DeclarativeInteractionGoodService implements OnDestroy { 8 | private stateSubscription = new Subscription(); 9 | private stateAccumulator = stateAccumulator; 10 | private stateSubject = new Subject